ほぼ2年前にTurbolinks紹介しました。全ページロードのウェブサイトを高速のSPA(シングルページアプリ)に変えてくれます。しかも、Laravelのフロントエンドのブレードやバックエンドのコントローラーをまったく変えることなしです。今回は、再びこのTurbolinksに戻って、まずcdnを使わずにnpmを使用してトランスパイルします。そして前回不可能と思っていたajaxによるフォームのPOST投稿もSPA化します。

SPA移行へのコストが非常に低いTurbolinks

ある程度大きな規模のプロジェクトを抱えていると、開発テクノロジー進歩によりプログラムを書き直す必要性が出てきます。それはまったく新しいデバイスへの対応(例:bootstrap)であったり、プログラムの管理性を高めるためのフレームワークの採用(例:Laravel)であったりします。プロジェクトが大きいほど、書き直しには時間とコストがかかります。スマホの対応ならお客さんにとっては理解されやすいですが、フレームワークの採用では外観は何も変わらないときもあるので、何か月(あるいは何年)もかけて書き直す必要性を説くのは大変です。

今回のSPAの採用も同じ状況です。SPAの採用の目的はユーザー体験の改善です。画面のリフレッシュが行われないのでページの遷移がスムーズで高速です。それだけで採用の説得は十分ですが、問題はSPAへ移行のためのコストが非常に高いことです。まず、すでにたくさんある、しかも新しいのが次々と登場する、SPAのJavascriptのフレームワークの中で、どれが有望性があるか長期的に管理されるかを見極めるために時間が必要なこと、さらにどれかに決めたらそれを学習して巧みになること、その上で既存のプロジェクトをSPAへ移行する開発です。これらを考慮すると、ReactJS, AngularJS, VueJSなどを採用してSPAに書き直すには莫大な時間、つまりコストがかかります。それらのフレームワークの経験に乏しい私が持つ小さい開発チームだと、特に。

そこで輝くのがTurbolinks!

Turbolinksを採用すると、ウェブページ内に同じサイト内のリンク、つまり<a href= ..>があると、そのリンクがクリックされたら、Turbolinksはajaxでそのページを取ってきてその中の<body>のコンテンツを現在のページのものと取り換えます。<head>は同じなので、全ページ更新とはならず、SPA特有のスムーズなページ遷移となります。しかも、ブラウザのURLはあたかも全ページ更新と同様に変更されるし、遷移したページ内のJavascriptも実行されます。さらに、ブラウザのWeb History APIを利用してキャッシュからの「戻る」ボタンで前のページの出力にも対応しています。これらは、既存のサーバーサイドのプログラムを変更することなく、次に説明する、npmでパッケージを追加するだけでできちゃいます。夢のような話と思いませんか?

npmでパッケージを追加

このTurbolinksを前回紹介したときは、

<html lang="ja">                                                                                                                                                                                                   
  <head>                                                                                                                                                                                                           
    <meta charset="utf-8">                                                                                                                                                                                         
    <title>Turbolinks</title>                                                                                                                                                                                      
    <script src="https://cdnjs.cloudflare.com/ajax/libs/turbolinks/5.0.3/turbolinks.js"></script>    
..

のようにCDNを使う設定でした。

今回は、npmでパッケージを追加して(関連記事)して、npmでトランスパイルします。

まず、コマンドラインでパッケージの追加。

$ npm install turbolinks --save-dev

次に、bootstrap.jsに以下の行を追加して、このパッケージをロードします。

...
var Turbolinks = require("turbolinks");
Turbolinks.start();

最後にトランスパイルの実行です(ライブバージョンは、npm run prod)。

$ npm run dev

これでホーム画面のリンクから以下の会員登録画面にアクセスすると、以下のようにすでにSPA化されています。画面の下は、FirefoxのInspectorのパネルですが、そこのxhrはXMLHttpRequest (XHR) のことでいわゆるajaxの実行のことです。

フォームの投稿のSPA化

さて、turbolinksは、リンクのようなGETに対しては自動にSPA化してくれますが、POSTは自動で対応していません。同じ登録画面で、入力して意図的にエラーを出すようにして、「Register」ボタンを押すと、以下のように通常の入力データの投稿のPOSTとエラーの出力のGETのアクションが起こります。

見ての通り、それらのアクションはajaxではありません。通常の全ページロードです。

turbolinksのレポのgithubサイトには、フォームの投稿後のリダイレクト、つまり入力が成功して違う画面に(例:ホーム画面)に飛ぶことに関して、ajaxを使用しての対応に関して書かれている部分があります。しかし、上のようにバリデーションエラーが発生して入力画面に戻りエラーを出力する場合は書かれていません。

つい最近まで、私も「そういうものなのか」「まあGETだけSPAになるだけでも結構」と思っていました。しかし、より調査したところ、Rubyではエラーの際に画面をレンダーするパッケージがある、ということを知りました。ちなみに、TurbolinksはRuby in Railsを開発したBaseCampが発明したものなので、とてもRubyとの仲が良い。

Rubyで可能なら、どこかにそれに対応するJavascriptの例があるのでは、と思い、以下にそれらしきディスカッションを見つけました。

https://github.com/turbolinks/turbolinks/issues/85#issuecomment-338784510

これをもとに、会員登録をSPA化してみます。

編集するのは以下のブレードです。

...
@section('script')

    $(document).on('turbolinks:load', function() { // Turbolinksがロード完了のイベントにおいて、
      $("form").on("submit", function(event) {
        event.preventDefault();

        $.ajax({ // jqueryのajax関数
          url: '/register', // データ送信宛てのURL
          type: 'POST',
          dataType: 'html',
          processData: false,
          contentType: false,
          cache: false,
          data: new FormData($("form")[0]) // フォームのデータを送信
        }).done(function(response, status, $xhr) {
          if (response && ($xhr.getResponseHeader('Content-Type') || '').substring(0, 9) === 'text/html') {
            var referrer = window.location.href; // 現在のURLを取得
            Turbolinks.controller.cache.put(referrer, Turbolinks.Snapshot.wrap(response)); // キャッシュにレスのデータを入れる
            Turbolinks.visit(referrer, { action: 'restore' }); // キャッシュを呼び出す
          }
        });
      });
    });

@endsection

そして、このscriptのセクションは、レイアウトのブレードに入れ込みます。

<!DOCTYPE html>
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}">
<head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">

    <!-- CSRF Token -->
    <meta name="csrf-token" content="{{ csrf_token() }}">

    <title>{{ config('app.name', 'Laravel') }}</title>

    <!-- Scripts -->
    <script src="{{ asset('js/app.js') }}" defer></script>

    <script type="module">
        @yield('script')
    </script>
...

これで再度、登録画面での入力エラーの実行をすると、

今度は、documentではなくxhrでの取得になっています。

さらに、登録成功のときは、以下のようにホーム画面への遷移もSPA化されています。

前回のVue.js:バックエンドのバリデーションを使うと、まったく同じ動作です。比べてみてください。

By khino