Если вы разрабатываете "современные" 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 может быть недостаточен для сложных интерфейсов, но для некоторых приложений он может облегчить и сэкономить время разработки.

Комментарии (5)


  1. Paskin
    07.08.2022 22:00
    -1

    Очевидно, Hotwire может быть недостаточен для сложных интерфейсов, но для некоторых приложений он может облегчить и сэкономить время разработки.

    Такое впечатление, что Ruby c Rails и прочими костылями - это средство производства потенциально "невыстреливающих" сайтов и приложений. Потому что в случае успеха вы либо разоритесь на хостинге - либо потратите кучу денег, переписывая все это добро "на ходу".
    Особенно весело это выглядит в "кровавом энтерпрайзе", да и любом другом месте - где одна и та же база данных служит источником для нескольких приложений.


    1. Rive
      07.08.2022 23:23
      +2

      В производительность базы данных можно упереться гораздо быстрее, а если решить её через шардирование, то ruby внутри каждого инстанса долго не будет узким местом даже несмотря на то, что он не самый производительный среди альтернатив.


  1. Medic84
    08.08.2022 07:38

    Чувство что RR разработчики придумали pjax и назвали это SPA...


  1. Paskin
    08.08.2022 09:06

    Я не про производительность - а про "заточенность" Рельсов на автогенерацию и автоизменение структуры базы.


  1. Doyal
    08.08.2022 20:40
    +2

    Вполне сносная вещь, для админок тем более. Определенно найдет свою нишу использования во фронтовой части.