Web サイトを SPA のような体験にする
Web サイトは高速ですが、クリックするたびに HTML ページ全体がリロードされています。伝統的な答えは、 シングルページアプリケーション (SPA)を構築することです。SPA は、別のスタックで書かれ、API と通信する JavaScript アプリケーションです。つまり、開発し、セキュリティを確保し、デプロイし、メインのアプリケーションと同期を保つべき 2 つ目のアプリケーションが必要になるということです。
Symfony には別の答えがあります。 Symfony UX は、サーバーサイドでレンダリングされるアプリケーションに SPA の体験をもたらすイニシアチブです。Twig テンプレートと Symfony コントローラーを書き続けながら、価値を生む場所にだけ JavaScript を振りかけます。
Symfony UX を発見する
良いニュースがあります。私たちは、この書籍の最初のステップから Symfony UX を使ってきました。webapp スケルトンには、Symfony UX のパッケージが 2 つ入っています。 symfony/stimulus-bundle と symfony/ux-turbo です。 importmap.php を見てみてください:
1 2 3 4 5 6
return [
'app' => ['path' => './assets/app.js', 'entrypoint' => true],
'@hotwired/stimulus' => ['version' => '3.2.2'],
'@symfony/stimulus-bundle' => ['path' => './vendor/symfony/stimulus-bundle/assets/dist/loader.js'],
'@hotwired/turbo' => ['version' => '8.0.23'],
];
Stimulus は、 data- 属性を通じて JavaScript の コントローラー を HTML 要素に接続する、小さな JavaScript フレームワークです。 assets/stimulus_bootstrap.js ファイルが Stimulus アプリケーションを起動し、 assets/controllers/ ディレクトリに格納されたコントローラーを自動的に登録します。
Turbo は、この基盤の上に構築されており、ナビゲーションを瞬時にします。
Turbo で瞬時にナビゲートする
Turbo Drive は、最初からずっと静かに働いていました。すべてのリンクのクリックとフォームの送信をインターセプトし、バックグラウンドでページを取得して、ページ全体をリロードすることなく <body> を入れ替えます。ブラウザは同じ JavaScript と CSS を保持し続けるので、ナビゲーションは瞬時に感じられます。
ブラウザの開発者ツールを開き、"Network" タブに切り替えて、Web サイト内をクリックしてみてください。ページの遷移は fetch リクエストであり、ドキュメント全体のロードではありません。
Turbo が見返りに求めるのは 1 つだけです。フォームに対する正しい HTTP セマンティクスです。送信が成功したらリダイレクトしなければなりません(私たちのフォームは redirectToRoute() のおかげでそうなっています)。そして、失敗した送信は 4xx ステータスコードを使わなければなりません。後者は Symfony が自動的に処理してくれます。送信されたフォームが無効な場合、 render() は 422 Unprocessable Entity ステータスコードでレスポンスを返します。
Turbo Frames でページの一部を更新する
Turbo Drive はページ全体のリロードを避けます。Turbo Frames はさらに一歩進んで、ページの一部分を残りの部分から独立してナビゲートできるようにします。
コメント一覧は完璧な候補です。"Previous" と "Next" リンクでコメントページを移動するときに変わるべきなのは一覧だけです。ヘッダーやフォーム、フッターを再レンダリングするのは無駄な仕事です。コメントをフレームで囲みましょう:
1 2 3 4 5 6 7 8 9 10 11 12 13 14
--- i/templates/conference/show.html.twig
+++ w/templates/conference/show.html.twig
@@ -17,3 +17,4 @@
<div class="row">
<div class="col-12 col-lg-8">
+ <turbo-frame id="comments" data-turbo-action="advance">
{% if comments|length > 0 %}
@@ -56,5 +57,6 @@
No comments have been posted yet for this conference.
</div>
{% endif %}
+ </turbo-frame>
</div>
<div class="col-12 col-lg-4">
<turbo-frame> 要素の内側にあるリンクは、そのフレームだけを更新します。"Next" をクリックすると、Turbo はバックグラウンドでページを取得し、対応するフレームを入れ替え、ページの残りの部分には手を付けません。
data-turbo-action="advance" 属性は、フレームのナビゲーションをページ全体の訪問へと昇格させます。アドレスバーの URL が(クエリーパラメータの offset も含めて)更新されるので、ページネーションされたコメントは共有可能なままですし、ブラウザの戻るボタンも期待通りに動作します。
Stimulus でコメントの写真をプレビューする
この書籍全体で初めての JavaScript を書くときが来ました。写真を投稿する参加者は、コメントを投稿する前に写真を確認できません。ファイルを選択したときにプレビューを表示しましょう。
Stimulus コントローラーを作成してください(ファイル名がコントローラー名 photo-preview を決めます):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
import { Controller } from '@hotwired/stimulus';
export default class extends Controller {
show() {
const [photo] = this.element.files;
if (!photo) {
this.preview?.remove();
this.preview = null;
return;
}
if (!this.preview) {
this.preview = document.createElement('img');
this.preview.className = 'img-thumbnail mt-2';
this.preview.style.maxHeight = '150px';
this.element.insertAdjacentElement('afterend', this.preview);
}
this.preview.src = URL.createObjectURL(photo);
}
}
必要な JavaScript はこれだけです。Stimulus はコントローラーを自動的に発見し、登録します。 data- 属性を通じて、コメントフォームの写真フィールドにコントローラーを接続してください:
1 2 3 4 5 6 7 8 9 10 11 12 13
--- i/src/Form/CommentType.php
+++ w/src/Form/CommentType.php
@@ -26,6 +26,10 @@ class CommentType extends AbstractType
->add('photo', FileType::class, [
'required' => false,
'mapped' => false,
+ 'attr' => [
+ 'data-controller' => 'photo-preview',
+ 'data-action' => 'change->photo-preview#show',
+ ],
'constraints' => [
new Image(maxSize: '1024k')
],
data-controller 属性がコントローラーをファイル入力フィールドにバインドし、 data-action が入力値が変わるたびに show() メソッドを呼び出します。カンファレンスページで写真を選択して、プレビューを楽しんでください。
Stimulus コントローラーは Turbo と手を取り合って動作します。Drive と Frames はページをリロードせずに更新するので、要素が DOM に入ったり出たりするのに合わせて、コントローラーは自動的に接続・切断されます。
モバイルアプリケーションはどうするのか?
私たちは、2 つ目のアプリケーションを構築することなく SPA の体験を手に入れました。別の JavaScript スタックも、2 つ目の Web サーバーも、CORS の設定も、新しくデプロイするものも何もありません。
そして、いつか ネイティブ モバイルアプリケーションが必要になったら、前のステップで作成した API が正しい入り口です。API は公開されていて、ドキュメント化されていて、フレームワークに依存しません。Web サイトとモバイルアプリケーションは、同じバックエンドを共有しながら独立して進化できます。