Turbo 6: Turboアプリケーションの構築

:::note
この記事は[Google翻訳](https://translate.google.co.jp/)の結果を編集したものです。
:::

https://turbo.hotwired.dev/handbook/building

Turboはリンクをたどったりフォームを送信したりするときにページ全体がリロードされるのを防ぐため高速です。アプリケーションはブラウザ内で永続的で長時間実行されるプロセスになります。これによりJavaScriptの構造を再考する必要があります。

特にナビゲートするたびに環境をリセットするために、ページ全体の読み込みに依存することはできなくなりました。JavaScriptの`window`と`document`オブジェクトはページが変更されても状態を保持し、メモリに残した他のオブジェクトはメモリに残ります。

このことを認識し少し注意を払うことで、Turboと密接に結合することなく、この制約を適切に処理するようにアプリケーションを設計できます。

# スクリプト要素の操作

ブラウザーは最初のページ読み込み時に存在するすべての`<script>`要素を自動的に読み込み評価します。

新しいページに移動するとTurbo Driveは新しいページの`<head>`内で現在のページに存在しない`<script>`要素を探します。 次にそれらを現在の`<head>`に追加し、ブラウザによって読み込まれて評価されます。 これを使用して、追加のJavaScriptファイルをオンデマンドでロードできます。

Turbo Driveはページをレンダリングするたびに、ページの`<body>`内の`<script>`要素を評価します。 インラインボディスクリプトを使用して、ページごとのJavaScriptの状態を設定したり、クライアント側モデルをブートストラップしたりできます。 ページが変更されたときに動作をインストールしたりより複雑な操作を実行したりするにはスクリプト要素を避け、代わりに`Turbo:load`イベントを使用します。

レンダリング後にTurboに評価させたくない場合は`<script>`要素に`data-turbo-eval="false"`で注釈を付けます。 この注釈は、ブラウザーが最初のページ読み込み時にスクリプトを評価することを妨げないことに注意してください。

## アプリケーションのJavaScriptバンドルをロードする

ドキュメントの`<head>`内の`<script>`要素を使用して、アプリケーションのJavaScriptバンドルを必ずロードしてください。 それ以外の場合、Turbo Driveはページが変更されるたびにバンドルをリロードします。

```
<head>
  ...
  <script src="/application-cbd3cd4.js" defer></script>
</head>
```

また、コンテンツが変更されたときに新しいURLを持つように、各スクリプトをフィンガープリントするようにアセットパッケージシステムを構成することも検討する必要があります。 その後、`data-turbo-track`属性を使用して、新しいJavaScriptバンドルをデプロイするときにページ全体を強制的にリロードできます。 詳細についてはアセット変更時のリロードを参照してください。

# キャッシングについて

Turbo Drive は、最近アクセスしたページのキャッシュを維持します。このキャッシュには2つの目的があります。復元アクセス中にネットワークにアクセスせずにページを表示することと、アプリケーションアクセス中に一時的なプレビューを表示することで知覚されるパフォーマンスを向上させることです。

（復元訪問を介して）履歴でナビゲートする場合、Turbo Driveは可能な場合はネットワークから新しいコピーをロードせずにキャッシュからページを復元します。

それ以外の場合、標準のナビゲーション中に（アプリケーション訪問を介して）Turbo Driveはキャッシュからページをすぐに復元しプレビューとして表示すると同時に、ネットワークから新しいコピーを読み込みます。これにより頻繁にアクセスされる場所のページが瞬時に読み込まれるように見えます。

Turbo Driveは新しいページをレンダリングする直前に現在のページのコピーをキャッシュに保存します。 Turbo Driveは`cloneNode(true)`を使用してページをコピーすることに注意してください。これは添付されたイベントリスナーと関連データが破棄されることを意味します。

## キャッシュするページの準備

Turbo Driveがキャッシュする前にドキュメントを準備する必要がある場合は`turbo:before-cache`イベントをリッスンします。 このイベントを使用してフォームをリセットしたり、展開されたUI要素を折りたたんだり、サードパーティのウィジェットを破棄したりして、ページを再び表示できるようにすることができます。

```javascript
document.addEventListener("turbo:before-cache", function() {
  // ...
})
```

Turboに要素をキャッシュさせたくない場合は要素に`data-turbo-cache="false"`という注釈を付けます。 これはフラッシュ通知などの一時的な要素に役立ちます。

## プレビューがいつ表示されるかを検出する

Turbo Driveはキャッシュからプレビューを表示するときに、`data-turbo-preview`属性を`<html>`要素に追加します。 この属性の存在を確認してプレビューが表示されているときの動作を選択的に有効または無効にすることができます。

```javascript
if (document.documentElement.hasAttribute("data-turbo-preview")) {
  // Turbo Drive is displaying a preview
}
```

## キャッシュのオプトアウト

ページの`<head>`に`<meta name="turbo-cache-control">`要素を含めキャッシュディレクティブを宣言することでページごとにキャッシュ動作を制御できます。

`no-preview`ディレクティブを使用してキャッシュされたバージョンのページがアプリケーションのアクセス中にプレビューとして表示されないように指定します。 プレビューなしとマークされたページは復元の訪問にのみ使用されます。

ページをまったくキャッシュしないように指定するには`no-cache`ディレクティブを使用します。 キャッシュなしとマークされたページは復元の訪問中も含め常にネットワーク経由で取得されます。

```html
<head>
  ...
  <meta name="turbo-cache-control" content="no-cache">
</head>
```

アプリケーションでキャッシュを完全に無効にするには、すべてのページに`no-cache`ディレクティブが含まれていることを確認してください。

## クライアント側からのキャッシュのオプトアウト

`<meta name="turbo-cache-control">`要素の値は`Turbo.cache`を介して公開されるクライアント側APIによっても制御できます。

```javascript
// Set cache control of current page to `no-cache`
Turbo.cache.exemptPageFromCache()

// Set cache control of current page to `no-preview`
Turbo.cache.exemptPageFromPreview()
```

要素がまだ存在しない場合、両方の関数は`<head>`に`<meta name="turbo-cache-control">`要素を作成します。

以前に設定されたキャッシュ制御値は、次の方法でリセットできます。

```javascript
Turbo.cache.resetCacheControl()
```

# JavaScriptビヘイビアーのインストール

`window.onload、DOMContentLoaded`または`jQuery Ready`イベントに応答してJavaScript動作をインストールすることに慣れているかもしれません。 Turboではこれらのイベントは最初のページの読み込みに応答してのみ発生し、その後のページの変更後では発生しません。 JavaScriptの動作をDOMに接続するための2つの戦略を以下で比較します。

## ナビゲーションイベントの監視

Turbo Driveはナビゲーション中に一連のイベントをトリガーします。 これらの中で最も重要なのは`Turbo:load`イベントです。このイベントは、最初のページの読み込み時に1回発生し、Turbo Driveにアクセスするたびに再度発生します。

`DOMContentLoaded`の代わりに`turbo:load`イベントを観察して、ページが変更されるたびにJavaScriptの動作を設定できます。

```javascript
document.addEventListener("turbo:load", function() {
   // ...
}))
```

このイベントが発生したときにアプリケーションが常に元の状態であるとは限らないことに注意してください。前のページ用にインストールされた動作をクリーンアップする必要がある場合があります。

また、Turbo Driveナビゲーションがアプリケーションのページ更新の唯一のソースではない可能性があるため、初期化コードを別の関数に移動して`turbo:load`やその他のDOMを変更できる場所から呼び出すことができます。

可能であれば`turbo:load`イベントを使用して他のイベントリスナーをページ本体の要素に直接追加することは避けてください。 代わりにイベント委任を使用してドキュメントまたはウィンドウにイベントリスナーを一度登録することを検討してください。

詳細については、イベントの完全なリストを参照してください。

## Stimulusにビヘイビアーを結びつける

新しいDOM要素は、フレームナビゲーション、ストリームメッセージ、またはクライアント側のレンダリング操作によっていつでもページに表示できます。これらの要素は多くの場合、新しいページの読み込みから来たかのように初期化する必要があります。

Turbo Driveページの読み込みからの更新を含む、これらすべての更新をTurboの姉妹フレームワークであるStimulus によって提供される規約とライフサイクルコールバックを使用して1か所で処理できます。

Stimulusを使用するとコントローラー、アクション、およびターゲット属性でHTMLに注釈を付けることができます。

```html
<div data-controller="hello">
  <input data-hello-target="name" type="text">
  <button data-action="click->hello#greet">Greet</button>
</div>
```

互換性のあるコントローラーを実装すると、Stimulusによって自動的に接続されます。

```javascript
// hello_controller.js
import { Controller } from "@hotwired/stimulus"

export default class extends Controller {
  greet() {
    console.log(`Hello, ${this.name}!`)
  }

  get name() {
    return this.targets.find("name").value
  }
}
```

Stimulusは[MutationObserver API](https://developer.mozilla.org/en-US/docs/Web/API/MutationObserver)を使用してドキュメントが変更されるたびにこれらのコントローラーと関連するイベントハンドラーを接続および切断します。 その結果、Turbo Driveページの変更、Turbo Framesナビゲーション、および Turbo Streamsメッセージを他のタイプのDOM更新を処理するのと同じ方法で処理します。

# 変換を冪等にする

多くの場合、サーバーから受信したHTMLへのクライアント側の変換を実行する必要があります。 たとえば、ユーザーの現在のタイム ゾーンに関するブラウザーの知識を使用して、要素のコレクションを日付別にグループ化することができます。

UTC で要素の作成時刻を示す`data-timestamp`属性を使用して一連の要素に注釈を付けたとします。 このようなすべての要素についてドキュメントをクエリーし、タイムスタンプを現地時間に変換し、新しい日に発生する各要素の前に日付ヘッダーを挿入するJavaScript関数があります。

この関数を`turbo:load`で実行するように構成した場合にどうなるかを考えてみてください。 ページに移動すると関数によって日付ヘッダーが挿入されます。 移動するとTurbo Driveは変換されたページのコピーをキャッシュに保存します。 ここで[戻る]ボタンを押します。Turbo Driveはページを復元し`turbo:load`を再度起動し、関数は日付ヘッダーの2番目のセットを挿入します。

この問題を回避するには変換関数を冪等にします。 冪等変換は最初の適用を超えて結果を変更することなく、安全に複数回適用できます。

変換を冪等にするためのテクニックの1つは、処理された各要素に`data`属性を設定して、変換を既に実行したかどうかを追跡することです。 Turbo Driveがキャッシュからページを復元するとき、これらの属性は引き続き存在します。 変換関数でこれらの属性を検出して、どの要素が既に処理されているかを判断します。

より堅牢な手法は単純に変換自体を検出することです。 上記の日付のグループ化の例では新しい区切り線を挿入する前に日付区切り線の存在を確認することを意味します。 このアプローチは元の変換によって処理されなかった新しく挿入された要素を適切に処理します。

# ページをロードしても要素を保持する

Turbo Driveを使用すると特定の要素を永続的なものとしてマークできます。 永続的な要素はページの読み込み後も維持されるため、これらの要素に加えた変更をナビゲーション後に再適用する必要はありません。

ショッピング カートを備えたTurbo Driveアプリケーションを考えてみましょう。 各ページの上部には現在カートに入っているアイテムの数を示すアイコンがあります。 このカウンターは項目が追加および削除されるとJavaScriptで動的に更新されます。

ここでこのアプリケーションで複数のページに移動したユーザーを想像してください。 彼女は商品をカートに追加しブラウザーの[戻る]ボタンを押します。 ナビゲーション時にTurbo Driveは前のページの状態をキャッシュから復元し、カートのアイテム数が誤って1から0に変更されます。

この問題は、カウンター要素を永続的としてマークすることで回避できます。 HTML`id`を指定し`data-turbo-permanent`で注釈を付けて永続的な要素を指定します。

```html
<div id="cart-counter" data-turbo-permanent>1 item</div>
```

各レンダリングの前にTurbo Driveはすべての永続的な要素をIDで照合し、それらを元のページから新しいページに転送してデータとイベントリスナーを保持します。