Tech Racho エンジニアの「?」を「!」に。
  • Ruby / Rails関連

Rails: Turbo Streamのしくみをピンポイントで理解する(翻訳)

概要

元サイトの許諾を得て翻訳・公開いたします。

日本語タイトルは内容に即したものにしました。

参考: Come Alive with Turbo Streams -- Turbo Handbook

Rails: Turbo Streamのしくみをピンポイントで理解する(翻訳)

Turbo Streamを使うと、Webリクエスト(コントローラアクション)に応じて特定の画面UIだけを更新できます。この機能はTurbo Streamと呼ばれています。
一方、バックエンドで何らかのイベント(モデルの作成・更新・削除)が発火したり、任意のオブジェクトから手動でイベントを発火させると、websocket経由で(通常はAction Cableで)Turbo Streamブロードキャストが配信されます。

Turbo StreamとTurbo Streamブロードキャストは、発生元は異なりますが、HTMLレスポンスはどちらも同じです。本記事では、Turbo Streamの仕組みを手短に解説することで、Railsと同様に、Turbo Streamにはいかなるマジック🎩🐰も仕込まれていないことを皆さんに理解いただきたいと思います。本当に、昔ながらの素のJavaScript(Plain Old JavaScript)しか使っていないのです!

Turbo Streamをブロードキャストするときは、モデルに以下のような感じで書きます。

class Resource < ApplicationRecord
  after_create_commit -> { broadcast_append_to "resources" }
end

コントローラのアクションを指定してブロードキャストする場合(もしくはお好みでコントローラ内でインライン形式で書きたい場合)は、以下のように書きます。

<turbo-stream action="append" target="resources">
  <%= render @resource %>
</turbo-stream>

2つのオプションで、レスポンスのHTMLはどう違うと思いますか?

<turbo-stream action="append" target="resources">
  <template>
    <!-- ResourceのHTMLコンテンツ -->
  </template>
</turbo-stream>

何と、コントローラのアクションを指定したときのTurbo Streamレスポンスと実によく似ているではありませんか!大きな違いがあるとすれば、パーシャルがレンダリングされるときにHTMLコンテンツが自動的に<template>要素でラップされていることだけです(これはパーシャルまたはViewComponentが由来です)。つまり、<template>要素は、非表示のHTMLコンテンツを保持するコンテナというわけです。

💡ブラウザのdevtoolsで以下のようにレスポンスを確認できます。

ブラウザのdevtoolsでresourceタブをハイライトしている様子

Turbo Streamの要素がDOMに注入されると、後はTurboが処理を引き継ぎます。<turbo-stream>は、HTML標準のカスタム要素に過ぎません。Turboではここで定義されています。
また、connectedCallback()関数はここで定義されています。この関数は、このカスタム要素がドキュメントに追加されるたびに呼び出されます。これはカスタム要素の機能です。

次は何が起きるのでしょうか?それではTurbo Streamの最も重要な部分を見ていくことにしましょう。振り落とされないように!🏎️💨

  1. beforeRenderEventというカスタムイベントがディスパッチされる(stream_element.js#L47
  2. Turboがこのイベント(stream_element.js#L163)を受け取ってrenderElement()関数(stream_element.js#L29)を呼び出す
  3. 続いてperformAction()関数(stream_element.js#L81)が呼び出される
  4. 続いて、定義済みのアクションが呼び出される(stream_actions.js#L4

最後のstream_actions.jsファイルには、Turbo Streamで使うデフォルトのアクション(action属性で指定されたappend()prepend()replace()などに応じて)がすべてサポートされていることがわかります。JavaScriptに少しでも詳しい方なら、それぞれのアクションで何が行われているかはすぐわかるはずです(わからない方は、拙著『JavaScript for Rails Developers 💡』をぜひチェックしてみてください)。

かいつまんで説明すると、<template>要素内のHTMLを取得して、(appendprependactionなどのアクションに応じて)DOMに追加しています(remove()だけはテンプレートを使わずに対象ノードを削除します)。

以上の知識を踏まえれば、以下のようにカスタムの<turbo-stream>要素をDOMに挿入するだけで、Turboがそれを察知して拾い上げてくれることを理解できると思います。

<html>
  <head>
    <script src="https://unpkg.com/@hotwired/turbo"></script>
  </head>
  <body>
    <ul id="resources">
    </ul>
  </body>

  <turbo-stream action="append" target="resources">
    <template>
      <li>
        <p>ぼくはTurbo Streamでappendされた! 🤯</p>
      </li>
    </template>
  </turbo-stream>
</html>

試しに上のHTMLをファイルにコピーしてブラウザで表示してみてください。<ul>要素にli要素がappendされている様子がわかります。🤯
次にブラウザのdevtoolsで、以下のような別の<turbo-stream>要素をDOMの「どこでもよいので」貼り付けてみてください。

<turbo-stream action="prepend" target="resources">
  <template>
    <li>
      <p>ぼくはTurbo Streamでprependされた!</p>
    </li>
  </template>
</turbo-stream>

実にクールですよね?Turboは、ブラウザのさまざまな機能を駆使して、開発者に愛されるスムーズな開発エクスペリエンスを提供してくれます。これであなたも、Turbo Streamのしくみをすっかり理解できました!

関連記事

Rails: Turbo Streamsをスムーズに遷移するturbo-transitionライブラリを公開しました(翻訳)

Rails: 特定ユーザー限定のコンテンツをTurbo Streamで送信する場合の注意事項(翻訳)

Rails: カスタムTurbo Streamでブラウザタブのカウンタをリアルタイム更新する(翻訳)


CONTACT

TechRachoでは、パートナーシップをご検討いただける方からの
ご連絡をお待ちしております。ぜひお気軽にご意見・ご相談ください。