【Rails7】図解でTurbo Frames入門

## はじめに

後掲のテキストは、SPA風のCRUDアプリ作りを通して、無料でTurboを学ぶことができているめちゃくちゃありがたいテキストです。
ただし、フロントエンドの知識がないと少々難しく、何が起きている？って状態でした。
デベロッパツールで通信の様子を確認すると理解が進んだため、図も併用しながらその様子を解説していきます。

https://zenn.dev/shita1112/books/cat-hotwire-turbo

### 対象読者

- Turboについて興味がある方
- Railsチュートリアル完走後、フロントエンド開発に興味がある方

### 動作環境

- Ruby - 3.1.0
- Rails - 7.0.2.3
- Google Chrome - 113.0.5672.63
- 上記テキストのチュートリアル1が終わったタイミングから解説します。
<details><summary>（お急ぎでcloneしたい方はこちら）</summary>

    ```
    $ git clone <https://github.com/shita1112/cat-hotwire-demo>
    $ cd cat-hotwire-demo
    $ git checkout -b tutorial-1  origin/tutorial-1
    $ yarn install
    $ bundle install
    $ rails db:create
    $ rails db:migrate
    $ rails db:seed
    ```
</details>

## Turboとは

Web開発においては以下の3つの技術で構成されておりDrive⇒Frames⇒Streamsの順に複雑になります。

- Turbo Drive
- Turbo Frames
- Turbo Streams

ここで、Turbo Driveに関するテキストの説明を引用します。

> 🐱 Turbo Driveは画面遷移を高速にしてくれるTurboの機能だよ
🐱 Turbo DriveはTurbolinksの名前を変えたもので、基本的な機能はTurbolinksと同じだよ。リンク、フォームのリクエストをTurbo Driveがインターセプトして、fetchによる非同期リクエストに差し替える。そしてレスポンスされたHTMLの<body>要素だけを抜き出して、現在のページの<body>要素を置換する。
🐱 通常の画面遷移がHTMLを丸ごと変えるに対して、Turbo Driveでの画面遷移は<body>だけを置換するよ（正確には<body>の置換に加えて、<head>の一部がマージされる）。
🐱 これの何が嬉しいかと言うと、画面遷移しても今のページのCSS・JavaScriptをそのまま利用できるため、CSS・JavaScriptを初期化してページに適用する処理をスキップできるんだ。これによって画面遷移が高速になるよ。
> 

すごくわかりやすいのですが、知らない単語があると難しいですよね。
Turboなし⇨Turbo Drive適用⇨Turbo Frames適用と、順を追って挙動を確認することで理解していきたいと思います。

## 通常の画面遷移（Turboなし）について

そもそも通常の画面遷移はどのようなことが起こっているのかデベロッパツールを使用して確認してみましょう。
1. 最初にTurboが無効になっていることを確認してください。

    ```app/javascript/application.js
    Turbo.session.drive = false // この記述があることを確認する
    ```

2. `$ bin/dev`でサーバーを立ち上げ、`/cats`にアクセスします。

3. GoogleChromeをお使いの方はF12を押してデベロッパツールを開き、Networkタブを選択した状態でページネーションを切り替えてみます。
![nomal.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1754248/547c90a0-2476-7f0d-6ea7-25ec65de645e.gif)

ページネーションを切り替えてみるとツール上で次のような複数のファイルが読み込まれたことがわかると思います。

| Name | ファイルの種類 |
| --- | --- |
| cats?page=4 | HTML |
| application~.js | JavaScript |
| assets/fonts/bootstrap-icons~ | CSS |
| assets/fonts/bootstrap-icons~ | CSS |
| favicon.ico | 画像 |

このように通常の画面遷移ではリクエストに応じてコンテンツに必要となる全てのファイルが読み込まれます（正確にいうとリクエストが5回発生している）

また、読み込まれたファイル選択し、タブを切り替えて詳細を確認してみましょう
![header.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1754248/f04ef407-9077-2e19-de39-789d4b686880.gif)
#### リクエストヘッダー
RequestURLは`http://127.0.0.1:3000/cats?page=4`となっています。これはページネーションの４をクリックしたからですね。そして、このリクエストにはオプションを追加するヘッダーが存在します。リクエストヘッダー内のAcceptは`text/html,application/xhtml+xml,application/`〜略となっています。このようにオプションを付与し、受け取ることができるフォーマットを明示して、サーバー側にリクエストを出しています。

#### レスポンスヘッダー
レスポンスヘッダーを見てみると`Content-Type: text/html; charset=utf-8`となっています。リクエストのAcceptに対応したフォーマットをサーバーが返していることがわかります。

#### レスポンスボディ
最後にレスポンスの中身を確認してみます。
![body.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1754248/8bc73855-1b5a-1b65-bb68-29919c6b0afc.gif)
プレビューではCSSが反映されていない、HTMLのレンダリング結果を確認することができます。そして、レスポンスボディ内でサーバーから返された名前のHTMLを確認できます。中身はレイアウトファイルである`application.html.erb`の内容が含まれた完全なHTMLです。

まとめると、通常の画面遷移ではページネーションでページを切り替えるなどのリクエストに対して、サーバーはページを構成する**HTMLやCSSなどの全ての要素、かつHTMLも完全なもの**を返しています。

## Turbo Driveで画面遷移を高速化

本題のTurbo Driveによる高速化を体感してみましょう。これまでは通常の画面遷移の様子を確認するために意図的にTurbo Driveを無効にしていましたが、有効に切り替えます

```app/javascript/application.js
Turbo.session.drive = false // この記述を削除する
```

F12でデベロッパツールを一度閉じ、再度開いた状態でページネーションを切り替えてみます。
![fetch.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1754248/0167d2ee-df77-0b49-519a-f0c21740b98f.gif)
いかがでしょうか。まず読み込まれたファイルの数が一気に２つに減りました。また、コンテンツのタイプがfetchとなっております。Turbo Driveが有効になると、リンクをクリックしたりフォームを送信する操作をTurbo Driveが検知して非同期(fetch)で通信を行うようになります。
非同期通信というのは、簡単にいうと`現在の画面の状態は変更せずに、バックグラウンドで通信を行う技術`です。Turbo Driveと組み合わせることで通信結果（レスポンス）が返ると画面に反映されます。
通常の画面遷移では通信中は画面を操作することができません。（現在はPCやブラウザの性能向上でそれほど体感できませんが。。）
#### Turbo Driveによるリクエストとレスポンス
通信結果のヘッダーとボディも確認してみましょう。
![drive-hb.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1754248/e09fe617-db77-9215-bb23-6f61bcb1e164.gif)
ここで変化があるのはリクエストヘッダー内のAcceptが`text/html, application/xhtml+xml`のみになっているところです。その他は特に変わりなく、レスポンスボディとしても**完全なHTML**を返していることがわかります。
#### Turbo Driveまとめ
ここまでくれば本題のTurbo Driveの内容が理解できると思います。

> 🐱 Turbo DriveはTurbolinksの名前を変えたもので、基本的な機能はTurbolinksと同じだよ。リンク、フォームのリクエストをTurbo Driveがインターセプトして、fetchによる非同期リクエストに差し替える。そしてレスポンスされたHTMLの`<body>`要素だけを抜き出して、現在のページの`<body>`要素を置換する。
> 
置換の様子を図に表すと次の通りです。
![body置換.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1754248/2c266e31-5ce9-d23b-d975-55683febccd8.png)
ページ切り替えの非同期リクエストに対して、レスポンスされたHTMLからTurboDriveが`<body>`要素を抽出してまるっと置き換えて、ページ切り替えを実現しているのです。

## Turbo Frames

Turbo Driveは`<body>`タグ全てを置換しますが、Turbo Framesは指定した領域のみを置換します。
ここではテキストで紹介のページネーションのTurbo Frames化について解説します。

TurboFrames化するには以下のように置換したい領域を`<%= turbo_frame_tag "cats-list" do %><% end %>`で囲い、`turbo_frame_tag`ヘルパーに任意のidを付与するよう引数を渡して指定します。

```html
# app/views/cats/index.html.erb
<div class="card-body mx-3">
    <%= turbo_frame_tag "cats-list" do %>
      <div class="row py-2">
        <div class="col-4 mt-auto">
          <%= sort_link(@search, :name) %>
        </div>
        <div class="col-4 mt-auto">
          <%= sort_link(@search, :age) %>
        </div>
        <div class="col-4 d-flex justify-content-end">
          <%= link_to icon_with_text("plus-circle", "登録"),
                      new_cat_path,
                      class: "btn btn-outline-primary"
          %>
        </div>
      </div>

      <%= render @cats %>

      <div class="d-flex justify-content-end mt-3">
        <%= paginate @cats %>
      </div>
    <% end %>
  </div>
</div>
```
#### Turbo Framesによるリクエストとレスポンス
先ほどと同様に通信の様子を確認してみましょう
![frames.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1754248/f8a4bce4-168d-3c0e-bbd7-ae4ad5420a0b.gif)
Trubo Drivesとの違いは３つあります。
- リクエストヘッダーに`Turbo-Frame: cats-list`が付与されることで、Turbo Framesを使用することを明示し、かつ置換する領域の`cats-list`を指定しています。ページネーションは`<%= turbo_frame_tag "cats-list" do %><% end %>`で囲われた領域に存在するため、リンクをクリックするとTurbo Framesが非同期通信を開始します。
- レスポンスボディとして`app/views/cats/index.html.erb`のコンテンツのみが返されています。これまでは`<head>`や`<body>`も含まれた完全なHTMLが返されていましたが、Turbo Framesリクエストにより、サーバーは**レイアウトコンテンツを除いたHTML**を返します。
- `turbo_frame_tag`ヘルパーにより以下のHTMLが生成されています。
    
    ```html
    <turbo-frame id="cats-list">
    	略
    </turbo-frame>
    ```
    

Turbo Framesによる置換の様子を図示します。
![frame.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1754248/7697315b-6a29-9062-0eda-b1f3b87e78ab.png)

ページを切り替えの非同期リクエストに対して、レスポンスされたHTMLからTurboFramesが`<turbo-fame>`要素を抽出し、対応するidの要素に置き換えてページ切り替えを実現しています。よって、同リクエストで置き換えられることがない**検索フォームなどは状態を保持したまま**にできるのです。

## 編集のTurbo Frames化

Turbo Framesのもう一つの事例として編集のインラン表示化の様子を確認してみましょう。

<details><summary>編集のTurbo Frames化を試す（お急ぎの方はこちら）</summary>

```
$ git checkout -b tutorial-2  origin/tutorial-2
$ git branch test-frames 753d984
$ git checkout test-frames
$ bin/dev
```

</details>

まず編集クリック前のHTMLを確認してみます。
![html.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1754248/72ea659f-c227-239a-b0f7-341b3def75ef.gif)
先ほど記述した`cats-list`下に`id="cat_320"`などの**独立したid**が付与された`turbo-frame`が**catオブジェクト分**生成されていることがわかります。

これは以下のように`turbo_frame_tag`にcatオブジェクトを渡すと、`turbo_frame_tag`ヘルパー内で利用されている`dom_id`ヘルパーが、`cat.id`から生成してくれているからなのです。

```html
# app/views/cats/_cat.html.erb
<%= turbo_frame_tag cat do %> # 内部でdom_idヘルパーを使用
  <div class="row py-2 border-top">
    <div class="col-4 my-auto">
      <%= cat.name %>
    </div>
    <div class="col-4 my-auto">
      <%= cat.age %>
    </div>
    <div class="col-4">
      <div class="d-flex justify-content-end">
        <%= link_to "編集", edit_cat_path(cat), class: "btn btn-sm btn-outline-primary me-2" %>
        <%= button_to "削除", cat, method: :delete, class: "btn btn-sm btn-outline-danger" %>
      </div>
    </div>
  </div>
<% end %>
```

編集クリック時に表示されるフォームも以下のように修正し、`turbo_frame_tag`にcatオブジェクトを渡しています。

```html
# app/views/cats/_form.html.erb
<%= turbo_frame_tag cat do %>
  <%= bootstrap_form_with(model: cat) do |form| %>
	略
  <% end %>
<% end %>
```

#### Turbo Framesによるリクエストとレスポンス - その２
これを踏まえた上で編集ボタンをクリックした時の通信の様子を確認してみます。
![frames-edit.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1754248/701685f0-4b8d-e7b9-925a-78b1572b7e7a.gif)
確認できるポイントは次の通りです

- リクエストヘッダの`Turbo-Frame: cat_320` これによりTurbo Framesを使用したリクエストを送信している
- レスポンスデータはレイアウトコンテンツを除いた`edit.html.erb`配下のコンテンツのみとなっている
- レスポンスデータのフォームは`<turbo-frame id=”cat_320”>`で囲われており、置換先の要素を指定している

Turbo Framesによる置換をイメージでも確認してみましょう
![frame-edit.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1754248/a3f4d606-1d36-e740-68b5-27b67da47117.png)

編集ページの非同期リクエストに対して、レスポンスされたHTMLからTurbo Framesが<turbo-frame id=”cat_320”>要素を抽出し、対応するidの要素に置き換えることで、より小さな要素単位で画面変更ができていることがわかりますね。


#### Turbo Framesまとめ
- `turbo_frame_tag`ヘルパーにより`<turbo-frame>`が生成される
- `turbo_frame_tag`ヘルパーへ、`@cat`などのオブジェクトを渡すことによって固有のidが付与される
- `<turbo-frame>`内のリンクがクリックされるとTurbo Framesリクエストが出され、サーバーはレイアウトコンテンツを除いたHTMLをレスポンスする
- レスポンス結果からTurbo Framesがidに対応する`<turbo-frame>`を置換する

テキスト内で続くバリデーション失敗時、更新成功時、キャンセル時も同様にして`<turbo-frame>`のid単位で置換を実施します。
## 最後に

バックエンドでデバッグツールを用いてメソッドなどの挙動を確認すると理解が深まるように、フロントエンド開発でもデベロッパツールを用いて通信の様子を確認することで理解が深まることが体感いただけたのではないでしょうか。Turbo Streamsに関しても同じように挙動を確認することができますので、ぜひ試してみてください！