このエントリは、SmartHR Advent Calendar 2023 の1日目です。
Turbo、使っていますか? Rails7から追加された新しいフロント用フレームワークで、思った以上にくすぐったい動きをして僕は結構好きです。今年のKaigi on RailsでもTurboに関するいくつかのセッションが発表され、その注目度が伺えます。しかしその注目度とは裏腹に、あまり周囲に使っている人を見かけません。まあたしかに業務に使うにはちょっと物足りないかなぁという気持ちもわからない事も無いのもありますし、また同時に現代ではReactやVueなどが主流になりすぎて使う気が起きないという理由もわかります。 とはいえ、使ってみないと海の物とも山の物ともつかぬままです。なので、とりあえずTurboを使ってみるのはいかがでしょうか? なんなら、TurboはRailsなしでも動きますし、とりあえず体験してみるのも良いんじゃないでしょうか。
そうです。Turboは、というよりもHotwireはRailsに一切依存しない独立したフレームワークです。Turbo本体とブラウザのJSエンジン以外に依存する物はありません。僕はTurboに結構良い意味でjQuery感を覚えているのですが、そういう意味でも結構似ています。今日は、Railsから取り外され、何にも依存していない素のTurboを触ってみて、どんな動きをするライブライなのかを見極めてみましょう。
なお、扱うトピックはTurboFrameとTurboStreamです。TurboDriveはわりとわかりやすいというか、TurboLinksをよりわかりやすくしただけの物なので割愛します。
今日のサンプルリポジトリ
cloneしてきて普通にブラウザでindex.htmlを開くか、もしくは完全に動かしたいのであれば同梱のSinatraアプリのスクリプトを実行してWebサーバーを立ち上げてください。
素のTurboを動かすには
Railsの場合はturbo-railsというgemで知らない間に入ってしまっていますが、素のTurboを使いたい場合はインストールが必要です。とはいえ、HTMLのheadの中に簡単なスクリプトを書くだけです。
<!DOCTYPE html>
<html>
<head>
<script type="module">
import hotwiredTurbo from 'https://cdn.skypack.dev/@hotwired/turbo';
</script>
</head>
</html>
これだけです。SkypackというビルドもやってくれるCDNから配信されます。このエントリを書いている令和5年11月30日(木)23:17の時点で、Turboは次期バージョンのTurbo8ベータ版が配信されているので、普通に壊れててビルドできませんでした。そのため、安定版の最新バージョンを指定してインポートします。
<!DOCTYPE html>
<html>
<head>
<script type="module">
import hotwiredTurbo from 'https://cdn.skypack.dev/@hotwired/turbo@7.3.0';
</script>
</head>
</body>
これだけです。素晴らしいですね。
以下、ただのHTMLにTurboを読み込んだだけの状態を、素Turobと勝手に呼びます。
TurboFrameの仕組み
TurboFrameは、簡単に言うと部分に絞ったTurboDriveです。HTMLのカスタム要素である <turbo-frame>
タグで囲まれた部分に対して、要素の差し替えを行うことで動的なページを表現します。
サンプルコードでは、こんなHTMLを用意しました。
<!DOCTYPE html>
<html>
<head>
<script src="https://cdn.tailwindcss.com"></script>
<script type="module">
import hotwiredTurbo from 'https://cdn.skypack.dev/@hotwired/turbo@7.3.0';
</script>
</head>
<body>
<div id="notification"></div>
<div class="container p-4">
<h1 class="text-2xl">素Turbo</h1>
<div id="buttons1" class="flex"></div>
<div class="pt-4">
<turbo-frame id="content">
<div>
<p>Rails7と一緒にリリースされたHotwire、その一機能であるTurboはRailsに依存しないフロントエンド用新しいフレームワークです。</p>
<p>よくRailsの機能はRailsに密結合していると誤解されますが、Hotwireを構成するTurbo, Stimulus, StradaはすべてRailsに直接依存しないライブラリですし、他にもRailsのコア機能の多くはポータブルに作れており、歴史的前後関係は逆になりますが、Railsというのはそれらを組み合わせた結果形になったフレームワークとも言えます。</p>
</div>
<div class="mt-4">
<a href="/next.html" class="bg-transparent hover:bg-blue-500 text-blue-700 font-semibold hover:text-white py-2 px-4 border border-blue-500 hover:border-transparent rounded">
Next
</a>
</div>
</turbo-frame>
<div id="buttons2" class="flex"></div>
</div>
</div>
</body>
</html>
本文のところが、<turbo-frame id="content">
というタグで囲まれていますね。このHTMLのNextリンクを押すと、次に読み込まれるHTMLは次のようになっています。
<!DOCTYPE html>
<html>
<head>
<script src="https://cdn.tailwindcss.com"></script>
<script type="module">
import hotwiredTurbo from 'https://cdn.skypack.dev/@hotwired/turbo@7.3.0';
</script>
</head>
<body>
<div class="container p-4">
<h1 class="text-2xl">素Turbo</h1>
<div class="pt-4">
<turbo-frame id="content">
<div>
<p>Turboがaタグの挙動を上書きしたことにより、Nextボタンを押すとここの文章が変わったはずです。ですが、アドレスバーのURLは変わっていません。</p>
<p>Turboはリンクのクリック挙動を監視し、ブラウザのページ遷移機能ではなくFetchAPIを使ってリンク先の内容を取得します。そして取得した内容に同じIDが指定された turbo-frame タグがあるか探します。turbo-frame タグがあった場合、現在のページの turbo-frame タグで囲まれた部分を、取得した内容の turbo-frame タグで囲まれた内容で上書きします。</p>
<p>ブラウザの開発者コンソールを開いて、通信内容を見ながらNextボタンを押してみてください。</p>
</div>
<div class="mt-4">
<a href="/incomplete.html" class="bg-transparent hover:bg-blue-500 text-blue-700 font-semibold hover:text-white py-2 px-4 border border-blue-500 hover:border-transparent rounded">
Next
</a>
</div>
</turbo-frame>
</div>
</div>
</body>
</html>
同じく、 <turbo-frame id="content">
というカスタム要素で本文が囲まれています。TurboFrameは、最初のページでリンクがクリックされたとき、通常のブラウザの挙動を乗っ取りFetchAPIを使ってリンク先の内容を取得します。そして、取得した内容に同じidのturbo-frameタグを発見すると、現在のページのturbo-frameタグの中身を取得したturbo-frameタグで置き換えます。これが、TurboFrameの基本動作です。TurboFrameは、FetchAPIをつかいリンクのクリックを監視することで、画面遷移時に最小限のDOM更新だけに留めることによって画面の更新を高速化したり動きを付けて見せているのです。
じゃあ、Railsでよく書くこういうコードって結局なんなのよ? って感じもしますよね。
<%= turbo_frame_tag @item do %>
<p><%= @item.name %></p>
<% end %>
これは turbo-rails gem が用意しているヘルパーで、ERBがレンダリングされた結果次のようなHTMLを返します。
<turbo-frame id="item_1">
<p>アイテム1</p>
</turbo-frame>
TurboFrameのキモは、ユニークなIDを持ったturbo-frameタグで囲まれた要素なので、turbo-railsはそれらを簡単に出力できるようにしているに過ぎないのです。このように、TurboはRailsなどの機能に一切依存せず、ただブラウザ上でHTMLの動作を監視しているだけです。そのため、どんなWeb Application Frameworkにでも組み込むことができるし、なんなら素TurboのようにWAFすら必要ないです。素Turboに実用性があるかどうかはさておきですが。
TurboFrameの良いところは、必ずしも完全なHTMLを返す必要は無くて、<turbo-frame>
タグで囲まれた部分的なHTMLを返してもきちんと動作します。サンプルリポジトリの中でも、それを解説したHTMLが置かれているので、是非確認してみてください。
TurboStream
TurboFrameでは、<turbo-frame>
タグという何となくわかりやすいルールがありましたが、そんなルール知るかよ、かかってこいとなるのがTurboStreamです。
まず前提として、TurboStreamはPOST/PUT/PATCH/DELETEのメソッドでのリクエストでしか動作しません。また、HTTPのレスポンスヘッダで、Content−TypeにContent-Type: text/vnd.turbo-stream.html; charset=utf-8
が設定されている必要があります。この二つの要素が揃わないと、TurboStreamは動きません。なので、実はTurboStreamは素Turboでは動かないんですよね。そこで、サンプルコードにはSinatraで書いた簡単なWebサーバーを同梱しています。別にSinatraとかRubyである必要は無く、POSTなどのメソッドに応答できて、Content-Typeを自分で書き換えられるものだったら何でも大丈夫です。
TurboStreamは、POST/PUT/PATCH/DELETのリクエストに対して、次のようなHTMLレスポンスを返すことで動作します。
<turbo-stream action="update" target="notification">
<template>
<div>
Hello Stream!
</div>
</template>
</turbo-stream>
TurboSteramは、<turbo-steam>
タグの属性でどのような動作をするかを定義しています。この場合、idがnotificationで特定できるDOM要素に対して、<tmplate>
で囲まれた中のDOMで上書きを掛けるという内容のTurboStreamです。TurboFrameが <turbo-frame>
タグの中身の書き換え専用だったのに対し、TurboSteramはidで特定できる要素であれば何でも書き換えられます。また、replace以外にもappendやremoveなどいくつかのアクションが用意されており、必要な物を選ぶことができます。
もう一つ大きな特徴は、一度に返せる <turbo-stream>
タグの数に制限はありません。そのため、画面上の複数の要素のDOMを一度のリクエストで書き換えることが可能です。サンプルコードでも実際にその動作の様子を見ることができると思います。このように、TurboStreamは副作用の生じるHTTPリクエストを行った結果、画面をダイナミックに変更する(例えば更新完了通知やエラー通知を出したり)場合に使用します。
以上のように、TurboStramもHTMLとHTTPの仕組みの上で動作する、非常に強力なDOM書き換えフレームワークです。なんかjQueryっぽいなと感じるのはこの部分ですが、jQueryと違ってサーバーから指令やDOMが飛んでくるので、元のHTMLがセマンティクスを維持したままとっちらかることがなさそうな点がメリットです。
素Turboの魅力
一切JSを書かず、HTMLとHTTPの仕組みだけでここまでの動的要素を作れること自体に感動を覚えます。いままでのRailsアプリにありがちな、いかにもRailsっぽい画面遷移とある程度お別れができる希望が見えてくるなと感じます。
そして何より、HotwireはRailsの仕組みではないということがわかるのが、素Turboの一番の魅力ではないでしょうか。
でもお高いんでしょう?
そうですね、決して安くはないと思います。まず、基本的にTurboはDOMのIDで要素を特定して操作するため、複数のパーシャルにIDの記述が散らばったりして、非常に管理が難しいです。特にTurboSteramの場合はそれが顕著になると思います。helperなどを書くなどである程度緩和はできるとは思いますが、それでもやはり書き換わる前や後などの状態を意識しながらID管理をするのはまあまあ大変じゃないかなと思います。
また、JSを書かなくてもSPA風の動作という大きなメリットもありつつ、別に普通にReact書けば良いじゃんそっちの方がリッチだよの意見には今のところぐうの音も出ません。やはりTurbo単体でできることには限りがあり、もう少し動的要素を加えるのであればStimulusを使って結局JSを書かなきゃいけないみたいなところも大いにあります。ですが、現在BetaのTurbo8などでは、新たにSteramにmorphというアクションが加わり、より表現が豊かになるなど、今後に期待できる技術だと思います。
完成されたデザインではなく、新しいアプリをとにかくユーザーが使いやすいように素早く作りたいとき、Turboは素晴らしい選択肢になると思います。