HTMLをいい感じに解釈してくれるTurbo Streams

前島真一氏:Turbo Streamsとは何かと言うと、下の図のようなHTML片をサーバーサイドからTurbo StreamsのContent-Typeで返してあげると、TurboがこのHTMLの内容を見て、いい感じに解釈して処理をしてくれるというものです。

いい感じに解釈するというのはどういうことかこのturbo-streamタグを使って説明しますね。turbo-streamタグにtarget属性があって、これはmessagesです。actionでそこに対して何をするかを書きます。これはmessagesでappendなので、ここのtemplateの中であるDOMをmessagesなidのDOMの中に挿入するという指示になります。

turbo-railsというgemを使うともうちょっと簡単にさっきのHTMLを構成できます。ここですね、turbo-railsを使うと、こんな感じでさっきのHTML片を作ることができます。

render turbo_stream : turbo-stream.append( : messages, # ゴニョゴニョ……)みたいな感じですね。このパーシャル以降は元となる部分テンプレートを指定します。さらにその部分テンプレートに対してturbo-streamタグや、テンプレートタグをここのヘルパーメソッドで追加して返してくれます。

なので、手動でturbo-streamタグを作る必要はないということですね。既存のmessagesを表現する部分テンプレートをそのまま活用して、Turbo Streamsを使うことができるということです。

今、appendするという操作だけを書きましたが、一度のリクエストやレスポンスで複数の操作をしたいケースもあると思います。この例はentry.turbo_stream.erbなんですが、この中に複数の処理を書くと……entryをremoveして、さらにappendするということが一度にできます。

さっきも話したとおり、actionとして使えるのはappend、prepend、replace、update、removeの5種類に限られています。SJRを使う場合と比べてできることは限られているので、考えることが減って楽です。他にもなんでもできるがゆえになんでもやってしまって、セキュリティに穴を開けてしまうことを防げるのがTurbo Streamsのメリットかなと思います。

Turbo Streamsのメリットはもう1つあって、それはwebsocketで効果的に使えるということです。Action Cable用のヘルパーメソッドがあって、Turbo Streams用のチャンネルのsubscribeやbroadcastが簡単にできます。

メールサービスでメール一覧を表示しているときに新着メールが届くことがあると思いますが、そのときにページをリロードせずに自動的に新しいメールをピュッと表示することが簡単に作れる感じですね。

このヘルパーメソッドがどうなっているのかは、ドキュメントにはあんまり書かれていないので、現時点ではソースコードを見る必要があります。

Turbo Framesでテンプレートの使い回しを可能にする

ここまでTurbo DriveとTurbo Streamsについて説明しました。Turbo Driveは、既存のHTMLをそのまま使いつつ、ユーザー体験を良くできるのがいいところです。Turbo Streamsのいいところの1つとして、同じテンプレートをいろいろなところで使い回せるというのがあります。

さっきの例で、一通のメッセージを表す部分のテンプレートをTurbo Streamsでも使えるというのがありました。

この使い回すというところをもっとやりたいじゃないですか。例えばスマホなどの小さい画面とPCなどの大きい画面があります。小さい画面と大きい画面で1つのテンプレートを使いたいというときにTurbo Framesが使えます。

Turbo Framesとは何かを説明します。turbo-frameタグがあります。このturbo-frameというタグの中身は、特別なframeとして区切られます。iframeとは別で、内部的なframeとして区切られます。このframe内のリンクはTurbo Frames用のリンクです。

このHTML自体はメッセージ一覧があって、個別のメッセージもズラッと表示されていると考えてください。ここでは1つしか表示されていないですけどね。

messageがあって、それを編集するためのリンクがあります。次のページのURLにアクセスしたときに返すHTMLがこれだとします。この中にもturbo-frameタグがありますね。idはさっきのturbo-frameのidと同じです。一覧ページの編集リンクを押すと、turbo-frameタグの中身だけが抜き出されて、さっきのturbo-frameタグの中身と差し替わります。

h1タグのところは無視されます。ここにmessage/1/editというURLがあります。ここに直接アクセスすると、ページが普通にレンダリングされます。このh1のメッセージの編集というのも含みます。

つまり、Turbo Driveはボディタグを全部差し替えるというもので、Turbo Framesを使うとレスポンスのHTMLのうちの一部を使って、元のHTMLの一部を部分的に差し替えることができるということです。

turbo-frameのリンク全部がそうだと困るというケースも当然あるので、そういうときには、target="_top"みたいなものを書くと、ここのframeのリンクは普通のTurboのリンクとなって、ボディタグ全体が書き替わります。

このtarget="_top"をうまく使い分けると、1つのテンプレートを使い回すのがやりやすくなります。例えばPCなどの大きいディスプレイを使ったときには一覧ページの中で編集をすることを簡単に実装できます。

一方でスマホのような小さい画面の中で、一覧ページで編集するのはなかなか大変ですよね。そういうときは専用のedit用のページに遷移させて、その中で編集できます。

レンダリングの時間をカットしてページの表示を早くする

Turbo Framesの便利な点はもう1つあります。srcという属性をつけることができることです。例では/maybe_friendsというのがありますが、このページが表示されたあとのタイミングで、ここ(maybe_friends)のURLにアクセスして、ここの返すHTMLの中のturbo-frameタグの中身が差し替えられます。結果、レンダリングにかかる時間を省略して、ページ全体を表示できて、ページの表示が早くなります。

このloading="lazy"は、DOMが画面に表示されるまで/maybe_friendsへのアクセスを遅延させるというやり方です。例えばturbo-frameタグが画面のすごく下のほうにあって、ファーストビューでは見えなければ、そこは表示しなくてもいいかもしれないですよね。もし表示しなかったら、その分マシンリソースの節約になります。こういう感じで、便利に使えるかなと思います。

ここまででTurbo Framesの説明は終わりで、ここからは最後のTurboですね。Turbo Nativeというものがあります。これはiOSやAndroidでTurboを使うためのライブラリです。iOSやAndroidでTurboを使うときは、webviewでアプリケーションを利用します。

Turbo Driveで画面遷移したときは、単にボディタグを差し替えるだけじゃなくて、ページを遷移した履歴も操作しているんですね。それをブラウザだけじゃなくて、iOSやAndroidでの画面遷移としても反映させるために使われるもので、機能としてそれ以上の特別なものがあるわけではないです。

そういう感じで、Turboについて一通り話をしました。冒頭から何回も言っているとおり、Hotwireを使うとサーバーサイドにロジックが集中して、jsの量はその分減ります。ただ、jsがまったくのゼロになることは今どきのアプリケーションを作っている限りはないですよね。

Turboを使うと、jsやCSSを最初のリクエストのレスポンスとして、全部返すという形式を取るのが基本的な戦略になると思います。そうすると、どこになんのjsを置くのかというのが難しくなってきます。さらに、特定のページだけでjsを実行させたいというのを実装するのが大変になります。if文がいっぱい入ってくるんですね。

HTMLとjsをいい感じに紐付けしてくれるStimulus

そういったことを解決するフレームワークとして、Hotwireの1つであるStimulusが活躍します。これはすごく簡単に説明すると、HTMLとjsをいい感じに紐づけあって整理するライブラリです。個人的には学習コストが低いのが気に入っています。

公式ページからスクリーンショットを撮ってきたものです。これをもとにして説明します。ここにinputタグがあって、willnetと入力してGreetボタンを押すと、ここにHello,willnet! と出てきます。

これは左側のHTMLと右側のjsで構成されています。左側のHTMLを見てみると、data-controllerとかdata-hello-targetとか、dataなんとかという属性がいっぱいあります。これがStimulusに関係している属性です。

この一番上のdivがdata-controller="hello"となっています。Stimulusはこれを見て、hello.controller.jsと紐づいていると判断して、ここのjsを見にいきます。このControllerを継承しているクラスがあるのを見にいきます。

次に左側のHTMLのbuttonのところですね。data-action="click->hello#greet"という属性がついています。ここのDOMをクリックすると、hello_controllerのgreetメソッドを実行する指示です。

つまり右側のgreetメソッドが実行されます。greetメソッドの中にはoutputTargetやnameTargetというプロパティがあって、これはstatic変数で定義されているnameとoutputから自動で生成されているプロパティです。

outputTargetはspanタグのdata-hello-target="output"というDOMと結びついています。右のnameTargetも同様ですね。上のinputタグのdata-hello-target="name"と紐づいています。

ここにwillnetが入っていって、結果として、Hello,willnet! が、ここのoutputTargetのtextContentとして入ってくる流れです。これがStimulusの基本的な機能です。すごくわかりやすいと思いませんか? もうだいたいわかった感じですよね。

Stimulusの便利ポイント

あともう1つStimulusの便利ポイントがあって、特定のページだけで発火させたいjsが書きやすいというのがあります。hello-controllerの中でconnectというメソッドを用意しておくと、hello_controllerに紐づいている属性のDOMが表示されたときに、自動的にここの処理が実行されます。

これがあると、特定のページでだけ実行させるのが簡単に書けます。

Stimulusのメリットは、controllerごとにファイルが自然と分かれて整理されることです。さらにHTMLタグの属性名を見ると、それがjsのどのファイルのどの場所、どのプロパティと紐づいているのかがすぐにわかります。

特定のページでだけ発火させたいjsが簡単に書けます。

TurboとStimulusに関して一通り話をしました。

Hotwireはフロントエンド側のRailsのようなもの

Hotwireについてまとめると、最小限の労力でユーザーが求めているサービスを提供することに特化しているライブラリだと思います。それはつまり、サーバーサイドでいうRailsですよね。それをフロントエンド側にも持ちこんだと言えると思います。

なので少人数で、どういう方針でサービスを成長させていくのか、開発しつつマーケットにフィットするかを試すようなスタートアップに一番適しているのかなと思います。さらに中規模くらいのサービスであれば、普通に使えると思っています。

すごく便利なので、みなさんも今回の機会に使ってみたらどうでしょうか? ちょっと長くなりましたが以上です。ご清聴ありがとうございました。