🕌

Turbo Frames とは何か?

2022/02/13に公開約6,000字

はじめに

Rails 7.0 で rails new すると、turbo-rails gemがインストールされ、app/javascript/application.jsimport "@hotwired/turbo-rails" と記載されています。turbo-railsとは何なのか検索してみると、どうやら Hotwire について知る必要があったので、調べてみたところ、さらに Hotwire も複数の要素から構成されていることがわかりました。

この記事では Hotwire を構成する要素の一つ Turbo Frames について試してみたことをまとめました。

https://zenn.dev/takeyuweb/articles/8ebe80bf442dc2

Hotwire

Hotwireは、JavaScriptを極力書かずにモダンなアプリケーションを作成するためのフレームワークです。開発者の好みのサーバー側プログラミング言語を使ってリッチなアプリケーションを作れるという触れ込みです。

Hotwire自体はライブラリではなく、実態は複数のライブラリを統合したものです。

詳しくは解説してくださっている記事がありますので、そちらをお読みください。

https://techracho.bpsinc.jp/hachi8833/2021_06_09/108495
https://logmi.jp/tech/articles/324219
https://zenn.dev/en30/articles/2e8e0c55c128e0

turbo-rails

TurboはHotwireの中心となる要素です。
TurboはTypeScriptで書かれたサーバー側言語に依存しないフレームワークですが、サーバー側言語との接続にはアダプタを書く必要があります。 Railsで使う場合はすでに用意されていて、便利なヘルパーを使うことができます。このヘルパーを実装するのが turbo-rails gem です。

Turbo Frames

Turbo を構成する要素の1つ Turbo Frames を使うと、

  • ページの部分書き換え
  • ページの一部の遅延読み込み

が可能になります。

<body>
  <h1>記事詳細</h1>

  <turbo-frame id="post_1">
    <h2>記事タイトルが入ります</h2>
    <p>記事内容が入ります</p>
    <p>(編集をクリックすると id="post_1" の部分が書き換わります)</p>
    <a href="/posts/1/edit">編集</a>
  </turbo-frame>
 
  <turbo-frame id="comments" src="/posts/1/comments">
    読み込み中。ここに記事コメントが入ります。(遅延読み込み)
  </turbo-frame>
</body>

ページの部分書き換え

ページ内のあらかじめ定義した部分(フレーム)を、要求に応じて更新できます。
リンクやフォームによりサーバーから取得したドキュメントから特定のフレームだけを取り出して既存のコンテンツを置き換えます

部分書き換え

たとえば次のようなHTMLで、id="post_1" のフレーム中にある『編集』リンクをクリックします。

<!-- GET /posts/1 -->
<body>
  <h1>記事詳細</h1>

  <turbo-frame id="post_1">
    <h2>記事タイトルが入ります</h2>
    <p>記事内容が入ります</p>
    <p>(編集をクリックすると id="post_1" の部分が書き換わります)</p>
    <a href="/posts/1/edit">編集</a>
  </turbo-frame>
 
  <turbo-frame id="comments" src="/posts/1/comments">
    読み込み中。ここに記事コメントが入ります。(遅延読み込み)
  </turbo-frame>
</body>

するとサーバーから次のような id="post_1" のフレームを含むHTMLが返されたとします。

<!-- GET /posts/1/edit -->
<body>
  <h1>記事編集</h1>

  <turbo-frame id="post_1">
    <form action="/posts/1" method="post">
      <div><input name="title" type="text" value="記事タイトルが入ります" /></div>
      <div><textarea name="body">記事内容が入ります
(編集をクリックすると id="post_1" の部分が書き換わります)</textarea></div>
      <button type="submit">更新</button>
    </form>
  </turbo-frame>
</body>

すると、Turbo によって表示中のページの id="post_1" フレームが書き換えられ、次のようになります。なお、ここでフォームを送信すると、同じように応答中の id="post_1" フレームでフレームを更新します。

<!-- GET /posts/1 -->
<body>
  <h1>記事詳細</h1>

  <turbo-frame id="post_1">
    <form action="/posts/1" method="post">
      <div><input name="title" type="text" value="記事タイトルが入ります" /></div>
      <div><textarea name="body">記事内容が入ります
(編集をクリックすると id="post_1" の部分が書き換わります)</textarea></div>
      <button type="submit">更新</button>
    </form>
  </turbo-frame>
 
  <turbo-frame id="comments" src="/posts/1/comments">
    読み込み中。ここに記事コメントが入ります。(遅延読み込み)
  </turbo-frame>
</body>

ポイント

  • サーバーから返却されるHTMLは断片ではなくページ全体
    • たとえば直接 /posts/1/edit にアクセスしても正しく機能する
  • リンクやフォームが自身のコンテキスト(どの turbo-frame か)を知っている
    • 応答中の同じ id のフレームで置き換える

落ち穂拾い

リンクやフォームでフレームではなく全体を更新したい

デフォルトでは、 turbo-frame タグ内のリンクやフォームは、そのフレームをターゲットにします。

そうではなく通常のリンクのようにページ全体を更新したい場合は、次のどちらかを使います。

  • turbo-frame タグの target 属性に _top を指定する
  • リンクやフォームの data-turbo-frame 属性に _top を指定する
<body>
  <div>
    メインコンテンツが入ります
  </div>

  <turbo-frame id="a" target="_top">
    <div>
      <!-- これらのリンクは全体を更新する -->
      <a href="/home/">ホーム</a>
      <a href="/aaaa/">AAAA</a>
      <a href="/bbbb/">BBBB</a>
    </div>
  </turbo-frame>

  <turbo-frame id="b">
    <div>
      <a href="/home/" data-turbo-frame="_top">このリンクは全体を更新する</a>
      <a href="/aaaa/">このリンクは id="b" を更新する</a>
    </div>
  </turbo-frame>
</body>
リンクやフォームで特定のフレームを更新したい

デフォルトでは、 turbo-frame タグ内のリンクやフォームは、そのフレームをターゲットにします。

そうではなく明示的にターゲットを指定するには、次のようにします。

  • リンクやフォームの data-turbo-frame 属性に対象のidを指定する
<body>
  <turbo-frame id="message_1">
    <a href="/messages/1/edit">編集 (このフレームで)</a>
  </turbo-frame>

  <form action="/messages/1/delete" data-turbo-frame="message_1">
    <button type="submit">このフォームを送信すると id="message_1" のフレームを更新する</button>
  </div>
</body>

ページの一部の遅延読み込み

ページ内のあらかじめ定義した部分(フレーム)を、ページ表示後に読み込むようにできます。
こうすることで、ファーストビューを高速に表示し、ユーザーの体感の待ち時間を小さくすることができます。

たとえば次のHTMLをサーバーが返したとします。

<!-- GET /posts/1 -->
<body>
  <h1>記事1</h2>

  <p>本文</p>

  <turbo-frame id="comments" src="/posts/1/comments">
    読み込み中。ここに記事コメントが入ります。(遅延読み込み)
  </turbo-frame>
</body>

読み込みが完了し、ブラウザ上で記事1と本文が表示されますが、この時点ではコメント部分は『読み込み中。ここに記事コメントが入ります。(遅延読み込み)』と表示されます。

Turbo は /posts/1/comments をフェッチして、サーバーから次の応答を得ます。

<!-- GET /posts/1/comments -->
<turbo-frame id="comments">
  <div>
    <div>
      <p>投稿者A<p>
      <p>コメント1<p>
    </div>
    <div>
      <p>投稿者B<p>
      <p>コメント2<p>
    </div>
    <div>
      <p>投稿者C<p>
      <p>コメント3<p>
    </div>
  </div>
</turbo-frame>

Turboは、応答に含まれる id="comments" フレームの内容を、既存の内容と置き換えます。
最終的なHTMLは次のようになります。

<!-- GET /posts/1 -->
<body>
  <h1>記事1</h2>

  <p>本文</p>

  <turbo-frame id="comments">
    <div>
      <div>
        <p>投稿者A<p>
        <p>コメント1<p>
      </div>
      <div>
        <p>投稿者B<p>
        <p>コメント2<p>
      </div>
      <div>
        <p>投稿者C<p>
        <p>コメント3<p>
      </div>
    </div>
  </turbo-frame>
</body>

Turbo Frames でできないこと

Turbo Frames では次のことができません。これを解決するには Turbo Streams を使います。

  • リンク、フォーム送信起因でしかDOM操作をできない
  • 複数フレームの同時更新をすることができない

落ち穂拾い

読み込み完了時に表示ががたつく

遅延読み込みされたコンテンツで置き換えたとき、要素の高さが変わることでブラウザ画面ががたつくのが気になることがあります。

このような場合はスケルトンスクリーンを使うのが良さそうです。

https://qiita.com/kanachimu/items/481c2459ef4ec298f47f

サンプル

https://github.com/takeyuweb/rails-turbo-frame-demo

まとめ

Turbo Frames を使うことで、JavaScriptを書くことなく部分更新と遅延読み込みを実現できます。

特に遅延読み込みはファーストビューの高速化やキャッシュヒット率の向上など快適性を向上させるのにとても便利です。

使いやすいのでぜひ使っていきたいですね。

Discussion

ログインするとコメントできます