知らないうちに使っているかも? Turbo Driveの実態に迫る
Hotwireは、デフォルトのアプリケーション構成でインストールされており、まずはTurbo Drive(以降Drive)の機能が有効になっています。このため、知らないうちにHotwireの恩恵に与っていたということもあるでしょう。このDriveの基本的な動作を見るために、まずはデフォルト構成のアプリケーションを作成して、動きを追ってみましょう。
アプリケーションの作成
Hotwireの動作に集中するために、5つの--skip-xxxxオプションを指定して不要な機能は外してしまうことにします。
% rails new hotwire_app --skip-action-cable --skip-action-text --skip-action-mailer --skip-action-mailbox --skip-jbuilder
次に、Scaffoldingで適当なモデル(日本語フィールドと英語フィールドからなるWordモデル)を作成し、新規作成ページでの動き(リクエストとレスポンス)を追ってみます。
% rails generate scaffold word japanese english % rails db:migrate % rails server
デベロッパーツールの準備
Pumaサーバが起動したら、Webブラウザ(Google Chrome)を起動します。アドレスバーに「http://localhost:3000/words」と入力し、項目はまだありませんがWordモデルの一覧を表示させておきましょう。ここで、デベロッパーツール(DevTools)を[縦三点リーダー(Google Chromeの設定)]―[その他のツール]―[デベロッパー ツール]で開いておきます。ウインドウ右側にデベロッパーツールのパネルが展開されます。
[NOTE]デベロッパーツールの設定
デベロッパーツールは、設定によってはウインドウ下側に配置されますが、今回は右側に配置した方が便利です。デベロッパーツールの[縦三点リーダー(Customize and controls DevTools)]アイコンから開くメニューから「固定サイド(Dock side)」の設定を変更してください。また、日本語化が可能なので、本稿では日本語化された状態を前提に解説を進めます。
パネルが展開されたら、上部の「要素」「コンソール」……と並んでいるバーの[ネットワーク]をクリックしておきます。これで、表示がネットワークタブに切り替わり、ネットワーク上のやりとりを見ることができます(図1)。
アプリケーションの操作
ここで、[New word]リンクをクリックして新規作成ページを表示させます(図2)。
表示が欠けている部分もありますが、パネル下のリクエスト一覧(「名前」「ステータス」「タイプ」……と並んでいる欄)のタイプ列に注目してください。「fetch」「stylesheet」「script」「vnd.microsoft.icon」となっています。このうち、「fetch」はリクエストがFetch APIに基づくものという意味です(他は、CSS、JavaScript、Iconという意味)。この意味は後述するとして、表示された新規作成ページで各フィールドを適当に入力し、[Create Word]ボタンをクリックして作成を実行してください(図3)。
すると、ページは項目追加後の詳細表示となり、リクエスト一覧にはさらに表示が増えます。今度は「fetch / リダイレクト」が追加され、さらに「fetch」が追加されます。ひとまず、新規作成までを行ってみました。
Driveの動作
ここまでで、内部で何が行われているかを以下にまとめてみました。
- [New Word]リンクのクリックで1回目のfetchリクエストが発生、新規作成ページが返される
- 新規作成ページで項目を入力し[Create Word]ボタンのクリックで2回目のfetchリクエストが発生、項目の保存後にステータスコード302とともにリダイレクト先が返される
- 3回目のfetchリクエストがリダイレクト先である「/articles/1」へ発生、追加した項目の詳細ページが返される
デベロッパーツールのネットワークタブに現れた「fetch」とは、JavaScriptのFetch APIによる非同期リクエストを意味しています。Ajaxなどで用いられたXmlHttpRequest(XHR)の発展型と思っておけばよいでしょう。このように、Driveが有効な状態では、リンクやボタンのクリックで、通常のGETメソッドやPOSTメソッドのリクエストが発生するのではなく、Fetch APIによる非同期リクエストが発生します。
Driveでは、基本的にアクションに応じてfetchリクエストを発行し、そのレスポンスでページ内容を更新します。このとき重要なのは、fetchリクエストなのでページの遷移は発生せず、レスポンスの内容で自らを書き換えて、URLも変更しているということです。
これらの処理はDriveの以下の機能が使われています。
- レスポンスはHTMLとなる。普通のAPI呼び出しのようなJSONではない
- レスポンスからbody要素のみを取り出し、現在のページのbody要素をそれで置き換える
- title要素やCSRF対策用トークンなどのhead要素の一部も書き換える
- JavaScriptのHistory APIを使ってURLも更新する
[NOTE]Driveのその他の機能
シンプルなデータでしかもローカルでアプリケーションを実行しているときには気付きにくいのですが、時間のかかるリクエスト(500ミリ秒以上)の待機中に表示されるプログレスバーの機能もあります。また、ページの内容をキャッシュしておきブラウザの[戻る][進む]で表示する機能、ページのリクエストが完了するまで暫定的にキャッシュの内容を表示しておくプレビューの機能もあり、良好なレスポンスに貢献しています。
Driveは明示的に無効にしない限りデフォルトで有効なので、link_toメソッドで作成したリンクや、form_withメソッドで作成したフォームには、基本的にDriveが作用することになります。なお、Driveを含むTurboはJavaScriptライブラリであり、app/javascript/application.jsにおいてリストのように読み込まれています。
import "@hotwired/turbo-rails" Hotwireの読み込み import "controllers"
Turboを無効にする
Turboは無効にすることができますので、Turboが有効なときの動作と比べることで、Turboの基本的な動作がより理解できるでしょう。Turboは、app/javascript/application.jsに以下の行を記述すれば無効にできます。
Turbo.session.drive = false
今回はfetchリクエストは発生しないはずなので、「フィルタ」と表示された検索ボックスの下の「ドキュメント」をクリックしておきます。これで、HTMLドキュメントのやりとりのみ表示できます。そして同じく、アドレスバーに「http://localhost:3000/words」と入力し、Wordモデルの一覧を表示させましょう。今度は、先ほど登録した1件が表示されていると思います。このとき、ネットワークタブのリクエスト一覧には、「タイプ」が「Document」であるリクエストが発生していることがわかります。これは、通常通りGETメソッドのリクエストが送られて、HTMLドキュメントが返されたことを意味しています(図4)。
Drive有効時と同じ手順を進めます。[New Word]をクリックして新規作成ページを表示させます。するとリクエスト一覧がいったんクリアされて、改めて「タイプ」が「Document」であるリクエストが発生したことがわかります(図5)。Driveが有効なときはページ遷移は発生しないので、アクションによってfetchリクエストの状況が蓄積されていきますが、無効なときにはアクションのたびにリクエスト一覧がクリアされていきます。
続けて、新規作成ページでフィールドを埋めて[Create Word]ボタンをクリックしてみても、リクエスト一覧がクリアされて同様のリクエストが発生していることがわかります(図6)。
なお、ここではアプリケーション全体でTurboを無効にしましたが、リンクやフォーム単位で無効にすることもできます。link_toメソッドとform_withメソッドに「data: { turbo: false }」を指定してあげると、そのリンクとフォームではTurboが無効になります。
<%= link_to "New user", new_user_path, data: { turbo: false } %>
[NOTE]要素を更新の対象外とできるdata-turbo-permanent属性
Driveではbody要素全体が置き換わるので、たとえば動画や音声などページの表示が完了した後でも動作している要素がある場合、ページ更新のたびに再生がリセットされては不自然です。このような要素にはdata-turbo-permanent属性を指定することで、更新の対象から外すことができます。注意が必要なのは、要素の識別のために必ずid属性を指定することです。