Turbo 4: Turbo Streamsで生き生きと

:::note
この記事は[Google翻訳](https://translate.google.co.jp/)の結果を編集したものです。
:::

https://turbo.hotwired.dev/handbook/streams

Turbo Streamsは自己実行する`<turbo-stream>`要素でラップされたHTMLのフラグメントとしてページの変更を配信します。各ストリーム要素はターゲットIDとともにアクションを指定して、その中のHTMLに何が起こるかを宣言します。これらの要素はWebSocket、SSE、またはその他のトランスポートを介してサーバーによって配信され、他のユーザーまたはプロセスによって行われた更新でアプリケーションを有効にします。`imbox`に届いた新しいメールはその好例です。

# メッセージとアクションのストリーミング

Turbo Streamsメッセージは`<turbo-stream>`要素で構成されるHTMLのフラグメントです。以下のストリームメッセージは可能な7つのストリームアクションを示しています。

```html
<turbo-stream action="append" target="messages">
  <template>
    <div id="message_1">
      This div will be appended to the element with the DOM ID "messages".
    </div>
  </template>
</turbo-stream>

<turbo-stream action="prepend" target="messages">
  <template>
    <div id="message_1">
      This div will be prepended to the element with the DOM ID "messages".
    </div>
  </template>
</turbo-stream>

<turbo-stream action="replace" target="message_1">
  <template>
    <div id="message_1">
      This div will replace the existing element with the DOM ID "message_1".
    </div>
  </template>
</turbo-stream>

<turbo-stream action="update" target="unread_count">
  <template>
    <!-- The contents of this template will replace the
    contents of the element with ID "unread_count" by
    setting innerHtml to "" and then switching in the
    template contents. Any handlers bound to the element
    "unread_count" would be retained. This is to be
    contrasted with the "replace" action above, where
    that action would necessitate the rebuilding of
    handlers. -->
    1
  </template>
</turbo-stream>

<turbo-stream action="remove" target="message_1">
  <!-- The element with DOM ID "message_1" will be removed.
  The contents of this stream element are ignored. -->
</turbo-stream>

<turbo-stream action="before" target="current_step">
  <template>
    <!-- The contents of this template will be added before the
    the element with ID "current_step". -->
    <li>New item</li>
  </template>
</turbo-stream>

<turbo-stream action="after" target="current_step">
  <template>
    <!-- The contents of this template will be added after the
    the element with ID "current_step". -->
    <li>New item</li>
  </template>
</turbo-stream>
```

すべての`<turbo-stream>`要素は含まれているHTMLを`<template>`要素内にラップする必要があることに注意してください。

WebSocket、SSE、またはフォーム送信への応答からの単一のストリームメッセージで任意の数のストリーム要素をレンダリングできます。

# 複数のターゲットを持つアクション

DOM ID参照を使用する通常の`target`属性の代わりに、CSSクエリセレクターを含む`targets`属性を使用して複数のターゲットに対してアクションを適用できます。例:

```html
<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は`text/vnd.turbo-stream.html`のMIMEタイプを宣言する`<form>`送信への応答として`<turbo-stream>`要素が到着したとき、自動的に`<turbo-stream>`要素を添付することを認識しています。`method`属性が`POST`、`PUT`、`PATCH`、または`DELETE`に設定されている`<form>`要素を送信すると、Turboは`text/vnd.turbo-stream.html`をリクエストの[Accept](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Accept)ヘッダーの一連のレスポンスフォーマットに挿入します。[Accept](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Accept)ヘッダーにその値を含むリクエストに応答する場合、サーバーは応答を調整して、Turbo Streams、HTTP リダイレクト、またはストリームをサポートしない他のタイプのクライアント （ネイティブ アプリケーションなど）を処理できます。

これはRailsコントローラーでは次のようになります。

```ruby
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はリンクを送信するとき、またはメソッドタイプが`GET`のフォームを送信するときに`text/vnd.turbo-stream.html`MIMEタイプを追加しません。アプリケーションで`GET`リクエストにTurbo Streamsのレスポンスを使用するには、リンクまたはフォームに`data-turbo-stream`属性を追加してTurboにMIMEタイプを含めるように指示できます。

# サーバーサイドテンプレートの再利用

Turbo Streamsの鍵は既存のサーバー側のテンプレートを再利用してライブで部分的なページ変更を実行できることです。最初のページの読み込み時にそのようなリスト内の各メッセージをレンダリングするために使用される HTMLテンプレートは、後で1つの新しいメッセージをリストに動的に追加するために使用されるテンプレートと同じです。これがHTML-over-the-wireアプローチの本質です。新しいメッセージをJSONとしてシリアル化したり、JavaScriptで受信したり、クライアント側のテンプレートをレンダリングしたりする必要はありません。これは再利用される標準のサーバーサイドテンプレートにすぎません。

これがRailsでどのように見えるかの別の例:

```html
<!-- 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 %>
```

```ruby
# 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アクションをレンダリングするために使用されます。これは次のようなレスポンスとして表示されます。

```html
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>
```

この`messages/message`テンプレート部分は編集/更新操作の後にメッセージを再レンダリングするためにも使用できます。またはWebSocketかSSE接続を介して他のユーザーによって作成された新しいメッセージを提供します。使用範囲全体で同じテンプレートを再利用できることは非常に強力であり、これらの最新の高速アプリケーションを作成するために必要な作業量を削減する鍵となります。

# 必要に応じて段階的に強化

Turbo Streamsを使用せずに対話設計を開始することをお勧めします。Turbo Streamsが利用できない場合と同じようにアプリケーション全体を動作させ、レベルアップとしてそれらを重ねます。これはフローの更新がなくてもネイティブアプリケーションまたは他の場所で動作する必要があるフローの更新に依存するようになることを意味します。

同じことが特にWebSocketの更新にも当てはまります。接続が不十分な場合、またはサーバーに問題がある場合、WebSocketが切断される可能性があります。アプリケーションがそれなしで動作するように設計されている場合、回復力が高くなります。

# しかし、JavaScriptを実行するのはどうでしょうか?

Turbo Streams は意図的にappend、prepend、(insert) before、(insert) after、replace、updateとremoveの7つのアクションにユーザーを制限します。これらのアクションが実行されたときに追加の動作をトリガーする場合は[Stimulus](https://stimulus.hotwired.dev/)コントローラーを使用して動作をアタッチする必要があります。この制限によりTurbo StreamsはHTMLをネットワーク経由で配信するという重要なタスクに集中でき、追加のロジックを専用のJavaScriptファイルに残すことができます。

これらの制約を受け入れることで、再利用できずアプリを追跡するのが難しくなる動作の寄せ集めで個々の応答を変えることができなくなります。Turbo Streamsの主な利点はその後のすべての更新でページの最初のレンダリングにテンプレートを再利用できることです。

# サーバーサイドフレームワークとの統合

Turboに含まれるすべての手法の中で、Turbo Streamsを使用するとバックエンドフレームワークとの緊密な統合から最大の利点が得られます。公式のHotwireスイートの一部として[turbo-rails gem](https://github.com/hotwired/turbo-rails)でこのような統合がどのように見えるかの参照実装を作成しました。このgemは、それぞれAction CableとActive Job フレームワークを介してRailsに存在する WebSocketと非同期レンダリングの両方の組み込みサポートに依存しています。

[Broadcastable](https://github.com/hotwired/turbo-rails/blob/main/app/models/concerns/turbo/broadcastable.rb)の問題をActive Recordに混合して使用すると、ドメインモデルから WebSocketの更新を直接トリガーできます。また、[Turbo::Streams::TagBuilder](https://github.com/hotwired/turbo-rails/blob/main/app/models/turbo/streams/tag_builder.rb)を使用すると、インラインコントローラーの応答または専用のテンプレートで`<turbo-stream>`要素をレンダリングし、単純なDSLを介して関連付けられたレンダリングで5つのアクションを呼び出すことができます。

ただし、Turbo自体は完全にバックエンドに依存しません。そのため、他のエコシステムの他のフレームワークにはRails用に提供された参照実装を見て独自の緊密な統合を作成することをお勧めします。

Turboの`<turbo-stream-source>`カスタム要素はその`[src]`属性を介してストリームソースに接続します。`ws://`または`wss://`URLで宣言すると基になるストリームソースは[WebSocket](https://developer.mozilla.org/en-US/docs/Web/API/WebSocket)インスタンスになります。それ以外の場合、接続は[EventSource](https://developer.mozilla.org/en-US/docs/Web/API/EventSource)を介して行われます。

要素がドキュメントに接続されると、ストリームソースが接続されます。要素が切断されると、ストリームが切断されます。

ドキュメントの`<head>`はTurboナビゲーション間で永続的であるため、ドキュメントの`<body>`要素の子孫として`<turbo-stream-source>`をマウントすることが重要です。

Turboによって駆動される典型的なフルページナビゲーションでは`<body>`が破棄されて結果のドキュメントに置き換えられます。ストリーミングが必要なページに要素が存在することを確認するのはサーバーの責任です。

あるいは、任意のバックエンドアプリケーションをTurbo Streamsと統合する簡単な方法は[Mercureプロトコル](https://mercure.rocks/)に依存することです。Mercureはサーバーアプリケーションが[Server-Sent Events (SSE)](https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events)を介して接続されているすべてのクライアントにページの変更をブロードキャストする便利な方法を定義しています。[Turbo StreamsでMercureを使用する方法を学びます。](https://mercure.rocks/docs/ecosystem/hotwire)