Если вы разрабатываете "современные" SPA приложения на Ruby on Rails, вы, скорее всего, используете какой-нибудь классный JS-фреймворк для быстрого обновления пользовательского интерфейса без перезагрузки страницы. И без JS фреймворка на фронтенде действительно мало что можно сделать, это своего рода стандарт в наши дни. Пока в Rails не появился Hotwire. С Hotwire вы можете получить быстрое и отзывчивое веб-приложение, но без написания тонны Javascript кода. Звучит здорово, но что такое Hotwire?
В этой статье мы рассмотрим основы Hotwire, а также создадим с его помощью тестовое приложение.
Какие опции есть для обновления Rails приложения без перезагрузки страницы?
25 июня 2013 года состоялся релиз Rails 4, в котором были представлены Turbolinks. Что Turbolinks делают для «отзывчивости» Rails? Turbolinks перехватывают все клики по ссылкам и вместо отправки обычного GET
запроса отправляют асинхронный запрос Javascript (AJAX) для получения HTML
. Затем Turbolinks мержит <head> тег выбранной страницы и заменяет весь <body>
тег страницы, поэтому полная перезагрузка страницы не требуется. Нет перезагрузки таблиц стилей или скриптов, что означает более быструю навигацию по страницам. Но он по-прежнему заменяет весь <body>
, а не только части страницы, которые изменились.
Но что, если вы хотите перезагрузить только те части, которые изменились? Вы можете использовать Rails AJAX helpers, то есть вы помечаете некоторые элементы как data-remote='true', что заставляет эти элементы отправлять запросы AJAX GET/POST
вместо обычных запросов GET/POST
. И Rails отвечает сгенерированным JS-кодом, который затем выполняется браузером для динамического обновления этих частей страницы.
Также мы можем использовать отдельные JS компоненты на фронтенде (например, с помощью React), чтобы сделать приложение еще более отзывчивым. То есть, JS компонент отправляет AJAX запрос, а сервер Rails отправляет в ответ JSON. Затем клиентский код преобразует полученный JSON в элементы DOM и обновляет его, чтобы отразить эти изменения. Это работает хорошо, единственным недостатком является то, что он смешивает рендеринг на стороне сервера и рендеринг на стороне клиента.
Другой, более традиционный способ сейчас — это использование React, Vue или другого JS-фреймворка на фронте, чтобы использовать только рендеринг на стороне клиента, то есть одностраничное приложение (SPA). При таком подходе фронтенд представляет собой отдельное приложение, которое отправляет AJAX запросы на сервер Rails, а Rails является исключительно JSON API. Наверняка вы знаете, что создание, поддержка и деплой двух отдельных приложений, которые обмениваются данными часто вызывает кучу проблем.
Но что, если бы вы могли работать с быстрым, отзывчивым приложением без всех сложностей создания двух отдельных приложений и написания большого количества кода Js? С этим вам может помочь Hotwire.
Что за Hotwire?
Hotwire — это альтернативный подход к созданию приложений типа SPA, в которых рендеринг HTML остается на стороне сервера (с использованием шаблонов Rails), при этом приложение остается быстрым и отзывчивым. Сохранение рендеринга на стороне сервера упрощает разработку и делает ее более продуктивной. Название Hotwire - это аббревиатура от "HTML Over the Wire", что означает отправку сгенерированного HTML вместо JSON от сервера к клиенту. Что также избавляет вас от необходимости написания кучи JS кода. Hotwire состоит из Turbo и Stimulus.
Что такое Turbo?
Turbo gem — является основой Hotwire. Это набор технологий для динамического обновления страницы, который ускоряет навигацию и отправку форм за счет разделения страниц на компоненты, которые можно частично обновлять, используя веб-сокеты в качестве транспорта. Если вы когда-либо работали с веб-сокетами в Rails, вы, скорее всего знаете, что Rails использует ActionCable для обработки соединения с веб-сокетами, и он включен в Rails по умолчанию. В свою очередь Turbo включает в себя: Turbo Drive, Turbo Frames и Turbo Streams.
Turbo Drive
Turbo Drive используется для перехвата кликов по ссылкам (так же, как Turbolinks делали это ранее), а также для перехвата отправки форм. Затем Turbo Drive мержит <head> тег страницы и заменяет <body> тег страницы. Точно также, как и Turbolinks, без полной перезагрузки страницы, которая может ускорить некоторые страницы, но не настолько, как ожидается от приложения в 2022 году, поэтому вы стоит рассмотреть обновления только отдельных частей страниц, а не всего <body>. На этот случай в Turbo используются Turbo Frames.
Turbo Frames
Сделать часть страницы Турбо-фреймом довольно легко, просто обернув его тегом турбо-фрейма с уникальным идентификатором.
<turbo-frame id=”13">
…
</turbo-frame>
Любое взаимодействие с элементами внутри такого фрейма отправляет запрос AJAX на сервер, и сервер, в свою очередь, отвечает HTML кодом только для этой части страницы. Что позволяет Turbo автоматически заменять только этот фрейм. Для этого не требуется писать код на JS. Но что, если вы хотите обновить несколько частей страницы одновременно? В этом вам помогут Turbo Streams.
Turbo Streams
Когда пользователь взаимодействует с элементом на странице (например, с формой/ссылкой), и Turbo Drive отправляет запрос AJAX на сервер, сервер отвечает HTML-кодом, состоящим из элементов Turbo Stream. Это является указаниями для Turbo, чтобы обновить затронутые части страницы. Турбо-потоки включают семь доступных действий: подставить в конце (append), подставить в начало (prepend), подставить перед элементом ((insert) before), подставить после элемента ((insert) after), заменить (replace), обновить (update) и удалить (remove)
:
<turbo-stream action="append" target="target_a">
<template>
HTML
</template>
</turbo-stream>
<turbo-stream action="prepend" target="target_b">
<template>
HTML
</template>
</turbo-stream>
<turbo-stream action="replace" target="target_c">
<template>
HTML
</template>
</turbo-stream>
Turbo Streams используют ActionCable для асинхронной доставки обновлений нескольким клиентам через веб-сокеты. Опять же, вы получаете все это без написания JS кода. Но даже если вам по какой-то причине нужен пользовательский Javascript (например дейтпикер), вы можете использовать Stimulus.
Что такое Stimulus?
Как и в Rails, где есть контроллеры с экшенами, Stimulus позволяет организовать код на стороне клиента аналогичным образом. У вас есть контроллер (JS объект), который определяет действия, то есть JS функции. Вы просто подключаете действие контроллера к интерактивному элементу на странице, используя атрибуты HTML. Затем действие триггерится, когда запускаются события DOM.
Сделаем простое приложение Rails используя Hotwire
Теперь, прочитав все вышеизложенное, можно задаться вопросом: как мне с этим работать? Hotwire довольно прост в использовании, все что нам нужно, это стандартное приложение Rails и сервер Redis. Сначала вам нужно установить Ruby 3, Rails 7 и сервер Redis, я не буду описывать процесс их установки, но вы можете легко найти любые инструкции, которые вам нужны, в зависимости от вашей платформы.
Итак, давайте настроим новое приложение Rails (мы будем использовать Bootstrap в качестве css фреймворка, просто чтобы приложение выглядело немного лучше):
rails new bookstore --css bootstrap
После того, как Rails сгенерирует все необходимые файлы, перейдите в каталог приложения:
cd bookstore
Приложение Rails 7 имеет все необходимое для начала использования Hotwire, Gemfile включает в себя: Redis gem, Turbo-rails gem и Stimulus-rails. Убедитесь, что у вас запущен и работает сервер Redis. Redis требуется, потому что он используется ActionCable для хранения информации, связанной с веб-сокетами. Адрес и порт по умолчанию для подключения Rails к серверу Redis задаются в config/cable.yml
development:
adapter: redis
url: redis://localhost:6379/1
Затем мы можем сгенерировать нашу модель, контроллер и миграцию, которые будут «Книгами» в нашем случае Книжного магазина. Он будет иметь тайтл, описание и счетчик лайков:
rails g scaffold books title:string description:text likes:integer
Давайте исправим сгенерированную миграцию, чтобы по умолчанию было 0 лайков для любой книги, которую мы добавляем в базу данных:
class CreateBooks < ActiveRecord::Migration[7.0]
def change
create_table :books do |t|
t.string :title
t.text :description
t.integer :likes, default: 0
t.timestamps
end
end
end
Не забываем создать базу данных для нашего приложения, запускаем в терминале:
rake db:create db:migrate
Давайте сделаем страницу списка книг главной страницей приложения, откроем config/routes.rb
и добавим отсутствующее объявление root пути:
Rails.application.routes.draw do
root 'books#index'
resources :books
end
Запускаем сервер Rails с помощью команды rails server
или ./bin/dev
(которая также будет отслеживать изменения css и js) в терминале, и когда вы зайдете на http://localhost:3000 в своем браузере, вы должны увидеть что-то вроде такого:
Давайте изменим паршиал app/views/books/_book.html.erb для книги на слудющее:
<%= turbo_stream_from "book#{book.id}" %>
<%= turbo_frame_tag "book_#{book.id}" do %>
<div style="background: lightblue; padding: 10px; width: 400px;">
<h2><%= book.title %></h2>
<p><%= book.description %></p>
<br>
<%= button_to "Like (#{book.likes})", book_path(book, book: { likes: (book.likes + 1) }), method: :put %>
</div>
<br/>
<% end %>
turbo_stream_from
указывает Hotwire использовать веб-сокеты для обновления фрейма, указанного с помощью :book_id
, а turbo_frame_tag
идентифицирует фрейм, который может быть заменен паршиалом при обновлении.
Чтобы сообщить Turbo, что мы хотим добавлять каждую новую созданную книгу в начало списка книг и обновлять количество лайков при каждом нажатии кнопки «Like», нам нужно добавить следующие коллбэки в файл app/models/book.rb
(также добавим валидацию):
class Book < ApplicationRecord
after_create_commit { broadcast_prepend_to :books }
after_update_commit { broadcast_replace_to "book_#{id}" }
validates :title, :description, presence: true
end
Первый коллбэк говорит Turbo использовать :books
Turbo стрим для обновления при создании книги, а второй говорит использовать :book_id
чтобы заменить шаблон при обновлении.
Затем исправим порядок книг в контроллере, а также добавим назначение переменной книги (чтобы мы могли добавлять новые книги на главной странице) в app/controllers/books_controller.rb
:
...
def index
@books = Book.order(created_at: :desc)
@book = Book.new
end
...
Мы также должны отредактировать шаблон главной страницы книг app/views/books/index.html.erb
, чтобы добавить Turbo стримы и Turbo фреймы:
<h1>Books</h1>
<%= turbo_stream_from :books %>
<%= turbo_frame_tag :book_form do %>
<%= render 'books/form', book: @book %>
<% end %>
<%= turbo_frame_tag :books do %>
<%= render @books %>
<% end %>
Чтобы избежать редиректов, когда мы добавляем книгу или обновляем существующую, нам нужно отредактировать create и update экшены в app/controllers/books_controller.rb
:
...
def create
@book = Book.new(book_params)
respond_to do |format|
if @book.save
format.html { redirect_to root_path }
else
format.turbo_stream { render turbo_stream: turbo_stream.replace(@book, partial: 'books/form', locals: { book: @book }) }
format.html { render :new, status: :unprocessable_entity }
end
end
end
def update
respond_to do |format|
if @book.update(book_params)
format.html { redirect_to root_path }
else
format.html { render :edit, status: :unprocessable_entity }
end
end
end
...
На данный момент наше приложение должно выглядеть так:
Каждый раз, когда вы добавляете новую книгу, используя форму на главной странице, Turbo добавляет ее в список книг, без перезагрузки страницы. Если вы откроете несколько вкладок в браузере — он обновит их все. Кнопки «Like» также работают без перезагрузки страницы и обновления количества лайков для книги на всех вкладках. И все это без единой строчки JS кода.
Заключение
Этот образец приложения является лишь базовым примером того, что вы можете делать с помощью Hotwire в Rails. Если вам нужно будет начать новое приложение Rails с обновлениями контента без перезагрузки страницы, в следующий раз, возможно, вам будет достаточно использовать Hotwire и Turbo, вместо того чтобы добавлять фронт на React или Vue. Очевидно, Hotwire может быть недостаточен для сложных интерфейсов, но для некоторых приложений он может облегчить и сэкономить время разработки.