Skip to content

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-bundlesymfony/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 を決めます):

assets/controllers/photo_preview_controller.js
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 サイトとモバイルアプリケーションは、同じバックエンドを共有しながら独立して進化できます。

This work, including the code samples, is licensed under a Creative Commons BY-NC-SA 4.0 license.
TOC
    Version