Добавляем жизнь с помощью Turbo Streams - geroi/turbo-rails GitHub Wiki

Turbo Streams передают изменения страницы в виде фрагментов HTML, обернутых в самоисполняющиеся элементы <turbo-stream>. Каждый элемент потока определяет действие вместе с идентификатором цели, чтобы объявить, что должно произойти с HTML внутри него. Эти элементы доставляются сервером через WebSocket, SSE или другой транспорт, чтобы оживить приложение обновлениями, сделанными другими пользователями или процессами. Отличным примером является новое письмо, приходящее в ваш почтовый ящик. ﹟Потоковые сообщения и действия

Сообщение Turbo Streams - это фрагмент HTML, состоящий из элементов <turbo-stream>. Приведенное ниже сообщение стрима демонстрирует семь возможных действий стрима:

<turbo-stream action="append" target="messages">
  <template>
    <div id="message_1">
      Этот div будет добавлен после элемента с идентификатором DOM "messages".
    </div>
  </template>
</turbo-stream>

<turbo-stream action="prepend" target="messages">
  <template>
    <div id="message_1">
      Этот div будет добавлен перед элементом с идентификатором DOM "messages".
    </div>
  </template>
</turbo-stream>

<turbo-stream action="replace" target="message_1">
  <template>
    <div id="message_1">
      Этот div заменит существующий элемент с DOM ID "message_1".
    </div>
  </template>
</turbo-stream>

<turbo-stream action="update" target="unread_count">
  <template>
    <!-- Содержимое этого шаблона заменит
    содержимое элемента с идентификатором "unread_count" путем 
    установив innerHtml в "", а затем переключившись в 
    содержимое шаблона. Любые обработчики, привязанные к элементу 
    "unread_count" будут сохранены. Это должно быть 
    в отличие от действия "заменить", описанного выше, где 
    это действие потребует перестройки  
    обработчики. -->
    1
  </template>
</turbo-stream>

<turbo-stream action="remove" target="message_1">
  <!-- Элемент с DOM ID "message_1" будет удален.
  Содержимое этого элемента потока игнорируется. -->
</turbo-stream>

<turbo-stream action="before" target="current_step">
  <template>
    <!-- Содержимое этого шаблона будет добавлено перед
    элемента с ID "current_step". -->
    <li>New item</li>
  </template>
</turbo-stream>

<turbo-stream action="after" target="current_step">
  <template>
    <!-- Содержимое этого шаблона будет добавлено после
    элемента с ID "current_step". -->
    <li>New item</li>
  </template>
</turbo-stream>

Обратите внимание, что каждый элемент <turbo-stream> должен обернуть свой включенный HTML внутри элемента <template>.

Вы можете отобразить любое количество элементов потока в одном потоковом сообщении из WebSocket, SSE или в ответ на отправку формы.

Действия с несколькими целями

Действия можно применять к нескольким целям, используя атрибут targets с селектором запроса CSS, вместо обычного атрибута target, который использует ссылку на DOM id. Примеры:

<turbo-stream action="remove" targets=".old_records">
  <!-- The element with the class "old_records" will be removed.
  The contents of this stream element are ignored. -->
</turbo-stream>

<turbo-stream action="after" targets="input.invalid_field">
  <template>
    <!-- The contents of this template will be added after the
    all elements that match "inputs.invalid_field". -->
    <span>Incorrect</span>
  </template>
</turbo-stream>

Потоковая передача из HTTP-ответов

Turbo умеет автоматически присоединять элементы <turbo-stream>, когда они приходят в ответ на запросы <form>, в которых объявлен MIME-тип text/vnd.turbo-stream.html. При отправке элемента <form>, атрибут method которого установлен в POST, PUT, PATCH или DELETE, Turbo вставляет text/vnd.turbo-stream.html в набор форматов ответа в заголовке Accept запроса. Отвечая на запросы, содержащие это значение в заголовке Accept, серверы могут адаптировать свои ответы для работы с Turbo Streams, HTTP-перенаправлениями или другими типами клиентов, которые не поддерживают потоки (например, нативные приложения). В контроллере Rails это будет выглядеть следующим образом:

def destroy
  @message = Message.find(params[:id])
  @message.destroy

  respond_to do |format|
    format.turbo_stream { render turbo_stream: turbo_stream.remove(@message) }
    format.html         { redirect_to messages_url }
  end
end

Повторное использование шаблонов на стороне сервера

Ключом к Turbo Streams является возможность повторного использования существующих шаблонов на стороне сервера для выполнения живых, частичных изменений страницы. HTML-шаблон, используемый для отображения каждого сообщения в списке при первой загрузке страницы, - это тот же шаблон, который будет использоваться для динамического добавления одного нового сообщения в список позже. В этом заключается суть подхода HTML-over-the-wire: Вам не нужно сериализовать новое сообщение как JSON, получать его в JavaScript, отрисовывать шаблон на стороне клиента. Это просто повторное использование стандартных шаблонов на стороне сервера. Еще один пример того, как это будет выглядеть в Rails:

<!-- app/views/messages/_message.html.erb -->
<div id="<%= dom_id message %>">
  <%= message.content %>
</div>

<!-- app/views/messages/index.html.erb -->
<h1>All the messages</h1>
<%= render partial: "messages/message", collection: @messages %>
# app/controllers/messages_controller.rb
class MessagesController < ApplicationController
  def index
    @messages = Message.all
  end

  def create
    message = Message.create!(params.require(:message).permit(:content))

    respond_to do |format|
      format.turbo_stream do
        render turbo_stream: turbo_stream.append(:messages, partial: "messages/message",
          locals: { message: message })
      end

      format.html { redirect_to messages_url }
    end
  end
end

Когда форма для создания нового сообщения отправляется на действие MessagesController#create, тот же самый частичный шаблон, который использовался для отображения списка сообщений в MessagesController#index, используется для отображения действия turbo-stream. В результате ответ будет выглядеть следующим образом:

Content-Type: text/vnd.turbo-stream.html; charset=utf-8

<turbo-stream action="append" target="messages">
  <template>
    <div id="message_1">
      The content of the message.
    </div>
  </template>
</turbo-stream>

Эти сообщения/части шаблона сообщения могут быть использованы для повторного отображения сообщения после операции редактирования/обновления. Или для предоставления новых сообщений, созданных другими пользователями через WebSocket или SSE-соединение. Возможность повторного использования одних и тех же шаблонов по всему спектру применения является невероятно мощной и ключевой для сокращения объема работы, необходимой для создания современных, быстрых приложений.

Постепенное улучшение, когда это необходимо

Хорошая практика - начинать проектирование взаимодействия без Turbo Streams. Заставьте все приложение работать так, как оно работало бы при отсутствии Turbo Streams, а затем добавьте их в качестве уровня. Это означает, что вы не будете полагаться на обновления для потоков, которые должны работать в нативных приложениях или в других местах без них. То же самое особенно верно для обновлений WebSocket. При плохом соединении или проблемах с сервером ваш WebSocket вполне может быть отключен. Если приложение рассчитано на работу без него, оно будет более устойчивым.

Но что насчет выполнения JavaScript?

Turbo Streams сознательно ограничивает вас семью действиями: append, prepend, (insert) before, (insert) after, replace, update и remove. Если вы хотите вызвать дополнительное поведение при выполнении этих действий, вы должны прикрепить поведение с помощью контроллеров Stimulus. Это ограничение позволяет Turbo Streams сосредоточиться на основной задаче - передаче HTML по проводам, оставляя дополнительную логику в специальных файлах JavaScript. Принятие этих ограничений позволит вам не превратить отдельные ответы в нагромождение поведений, которые невозможно повторно использовать и которые делают приложение трудноуправляемым. Ключевым преимуществом Turbo Streams является возможность повторного использования шаблонов для начальной визуализации страницы во всех последующих обновлениях.

Интеграция с фреймворками на стороне сервера

Из всех методов, входящих в Turbo, именно в Turbo Streams вы увидите наибольшее преимущество от тесной интеграции с вашим бэкенд-фреймворком. Как часть официального пакета Hotwire, мы создали эталонную реализацию того, как может выглядеть такая интеграция, в геме turbo-rails. Этот гем опирается на встроенную поддержку WebSockets и асинхронного рендеринга, присутствующую в Rails через фреймворки Action Cable и Active Job, соответственно.

Используя концерн Broadcastable, встроенный в Active Record, вы можете вызывать обновления WebSocket непосредственно из вашей доменной модели. А используя Turbo::Streams::TagBuilder, вы можете отображать элементы <turbo-stream> во встроенных ответах контроллера или специальных шаблонах, вызывая пять действий с сопутствующей визуализацией через простой DSL. Однако сам Turbo полностью независим от бэкенда. Поэтому мы рекомендуем другим фреймворкам из других экосистем обратиться к эталонной реализации, предоставленной для Rails, чтобы создать свою собственную тесную интеграцию.

Альтернативным способом интеграции любого бэкенд-приложения с Turbo Streams является протокол Mercure. Mercure определяет удобный способ для серверных приложений транслировать изменения страниц всем подключенным клиентам с помощью событий, отправляемых сервером (SSE). Узнайте, как использовать Mercure с Turbo Streams.

⚠️ **GitHub.com Fallback** ⚠️