defrag.works

海外の技術ブログで気になったものを日本語訳してメモしています

Turbo 8を8分で

fly.io

Turbo 8はライブ更新するRailsアプリケーションの開発を簡素化します。Turbo FramesとTurbo Streamsのレスポンスを手作業でコーディングする必要性を最小限に抑えることで、以前のバージョンから飛躍的に進歩しています。この進歩によってRailsアプリケーションの開発と保守の両方が簡素化され、生産性がさらに向上します。

Turboをご存じない方もいらっしゃるかもしれませんが、これはRuby on Railsアプリケーションで広く使用されているライブラリで、ページを部分的に更新してシングルページのJavaScriptアプリケーションと同じように応答性を感じさせるものです。HTMXStimulusReflexPhoenix LiveViewLaravel LiveWireなどのフレームワークに似ています。

Turbo 8は本当にスマートなページリローダーだと考えてください

これは単純化しすぎですが、この例えはTurbo 8の仕組みをよりよく理解するのに役立ちます。考え方はこうです:

  1. Railsはデータが変更されるとパブリッシュします。 broadcasts_refreshesを持つRailsモデルはActionCableを介してモデルが作成、更新、破棄されるとパブリッシュします。
  2. ページが気になるデータ変更を購読します。 ページが読み込まれると、Turbo JavaScriptがスキャンして<turbo-cable-stream-source/>タグを探します。各タグにはActionCable経由のデータ変更通知を購読するために使用されるモデルクラスとIDが記述されています。
  3. モデルが更新されるとサブスクライブされたページは何かが変更されたという通知を受け取ります。 TurboはバックグラウンドでHTMLページ全体をHTTP経由でリクエストし、新しいHTMLと現在ロードされている古いHTMLを比較します。HTMLファイル間に差異がある場合、ページ全体をリロードすることなく、差異のみをページに適用します。

それだけです。これがフレームワークです。印象的なのは、その印象の薄さです。私がこれまで見てきたチュートリアルのほとんどはTurbo 7とTurbo 8の比較にとらわれていますが、それは理解を難しくしていると思うのでTurboの古いバージョンについて知っていることはすべて忘れて、ベータ版を試すことで今後どのように動作するかを見てみましょう。

Turbo 8ベータ版のインストール

以下をGemfileに追加するか既存のturbo-railsエントリーを更新して、Turbo 8 gemをインストールしてください。

# Hotwire's SPA-like page accelerator [https://turbo.hotwired.dev]
gem "turbo-rails", "~> 2.0.0.pre.beta"

Railsサーバーを再起動して準備を整えましょう!

ページの先頭にTurboタグを追加する

最初に行う必要があるのはアプリケーションレイアウトの<head/>タグにタグを追加することです。

<%= turbo_refreshes_with method: :morph, scroll: :preserve  %>
<%= content_for :head %>

これによりTurbo 8の動作がページを「モーフィング」してスクロール位置が保持されるように設定されます。Turboの「古い」動作はページ全体を「置き換え」スクロール位置を「リセット」します。

コンテンツページを読み込むには依然としてTurboの「従来の」動作が必要です。コンテンツページのようにすべてにmethod: :morph, scroll: preserveを適用すると、ユーザーがコンテンツをクリックしたときにページがトップではなく真ん中から始まるという奇妙な動作が発生します。

ページがモデルに対してサブスクライブすることで変更を常に通知し、それに応じてリロードします。

ビューを更新したいとき、アプリケーションのビューファイルからサブスクライブします。例えば作者が変更を公開したときに更新したいブログ記事がある場合、./app/views/post/show.html.erbの先頭に次のように追加します。

<%= turbo_stream_from @post %>
<h1><%= @post.title %></h1>

このヘルパーは、HTMLにこのようなタグを出力します:

<turbo-cable-stream-source channel="Turbo::StreamsChannel" signed-stream-name="IloybGtPaTh2YzJWeWRtVnlMMVZ6WlhJdk1RIg==--b4bcfff51ae4074540fdefbada55a237d68206bf960bd30a6684b310a255656c" connected=""></turbo-cable-stream-source>

不可解に見えるかもしれませんがsigned-stream-name属性はモデルクラス内にあり、Turboが変更を購読するIDです。投稿が更新されるとTurboはサーバーから「このブログの投稿が変更されました」というシグナルを受け取ります。そしてJavaScript経由で現在のHTMLページにHTTPリクエストを行い、新しいHTMLをすでにロードされているDOMに差分し、ページをリロードする必要がないように2つの変更をマージします。

素晴らしい!これで変更をサーバーにサブスクライブするクライアントができました。Postモデルに変更を公開するようRailsに指示する必要があるので、Postモデルに以下を追加します。

class Post < ApplicationRecord
  # When the model instance is changed, a message will sent over
  # ActionCable that notifies the page to reload.
  broadcasts_refreshes
end

これでPostモデルを作成、更新、破棄するとRailsはそれをActionCableでパブリッシュし、変更があった場合はすべての関連ページにリロードするよう通知します。

コレクションを更新する方法

通常、コレクションはアプリケーション内のいずれかに属しています。例えばブログにはたくさんの投稿があります。ブログの全投稿を一覧表示するビューが./app/views/blog/posts/index.html.erbのどこかにあるでしょう。

<%= turbo_stream_from @blog %>
<%= render @blog.posts %>

そしてBlogに属するという関連付けをPostモデルに追加します。重要なのはtouch: trueを追加することです。

class Post
  # Touch will update the timestamp on the blog when
  # a post is created, updated, or destroyed.
  belongs_to :blog, touch: true

  # When the model is changed, a message will sent over ActionCable.
  broadcasts_refreshes
end

それからBlogモデルは更新をブロードキャストする必要があります:

class Blog
  has_many :posts
  broadcasts_refreshes
end

投稿が作成、更新、削除されるとBlogモデルはタイムスタンプを更新して、Blogインスタンスへの変更をリッスンしているページの更新をトリガーします。

何にも属さないコレクションについてはどうでしょうか?

実際にはこれはアプリケーションではまれなことです。例えばブログはおそらくユーザーやアカウントに属しており、上記と同様に "タッチ "することができます。以下はその例です。

class Blog < ApplicationRecord
  # Code from above removed for clarity
  belongs_to :user, touch: true
end

そしてUserモデルにbroadcast_refreshesを追加します。

class User < ApplicationRecord
  has_many :blogs
  # Blog will touch the account when something is changed.
  broadcasts_refreshes
end

そして全ユーザーのブログが一覧表示されるダッシュボード・ページで、アカウントの変更を確認します:

<%= turbo_stream_from current_user %>
<%= render @current_user.blogs %>

アプリケーションに親オブジェクトでモデル化できないコレクションがある場合でも、Turbo Streamsを使用してリストに追加することができます。

PostgresまたはSQLiteを使用して変更をブロードキャストする

Turbo 8ではHTMLペイロードをWebSocket経由でプッシュする必要がないため、Postgres ActionCableアダプタの8000バイト制限はもはや問題ではありません。小規模から中規模のアプリケーションでRedisまたはActionCable pub-subのみを使用している場合、依存関係としてRedisを排除することでアプリケーションのインフラストラクチャを簡素化できます。

SQLiteアプリケーションを本番環境にデプロイする場合、RailsアプリケーションにLitestackをインストールし、Litecableを使ってTurbo 8上で変更通知を発行することができます。

Rails開発の未来にもう一度ワクワクしよう

Railsアプリケーションを自動更新するために必要な労力は驚くほどわずかです。Turbo 8はまだベータ版であり、Railsアプリケーションを構築するためのこのアプローチには考慮すべきエッジケースがたくさんありますが、この技術はRailsアプリケーション開発をさらに簡素化する有望な方法になりつつあります。

Turbo 8より前のバージョンでTurbo Framesに大きく投資している場合、おそらく移行で一番大変なのはコントローラコード内のformat.turbo_streamブロックとビューのturbo_frameタグをすべて削除することでしょう。何千行ものコードを削除してgitコミットすることが生産的だと考えているのであれば、非常に生産的な開発の日々が待っていることを覚悟してください。