🧊

SPAのキホンのキ

SPAとはなにかをざっくり説明する記事です。

対象読者は、

  • SPAってよく聞くけど実際どういう仕組みかわかってない
  • SPAってのをつくろうと思うけどどこから手を付ければいいかわかってない
  • いわゆるサーバーサイドエンジニアで、クライアントのことを少し知っておきたい

みたいな人へ。
知ってる人にとっては今さら過ぎる内容ですが、知らない人にとってはこういうのすら知らないらしいので。

SPAとは

SinglePageAppの略です。
Webサイトの構築手法のことで、比較されるものとしては従来通りページ遷移するものがあります。

  • トップページ
  • 詳細ページA
  • 詳細ページB

ってな構成だった場合に、
SPAで作る場合はhtmlが1つあれば良くて、ページのURLも1つでOK。
ページ遷移する場合はhtmlが3つ、ページのURLも3つになります。

正確には

URLが1つで良い代わりに、URLの代わりに "#" を付けてページそれぞれを区別する必要があります。

ページ名 ページ遷移 SPA
トップページ example.com/top example.com#!/top
詳細ページA example.com/item/a example.com#!/item/a
詳細ページB example.com/item/b example.com#!/item/b

最近ではHistoryAPIなる仕様があり、 "#" なんか付けず綺麗なURLで同じことができるのですが、
Androidが元気なうちは大人しく "#" でやるのがベターです・・。

ようは、URLを変えず"#"を操作することで擬似的にページ遷移を表す仕組みがSPAのキモです。
それ以外は、今までと一緒です。

"#"の横の"!"なんやねんって思う人もいるかもしれませんが、特に意味は無いです。
なんかSPAですよってことを表すために使う通例だとかいう説が。

hashchangeを拾う

というわけで、URLに対してHTMLをサーバーから返す要領で、"#"が変わったタイミングを捕捉してページの表示を変更します。

window.addEventListener('hashchange', function() { /* ハッシュが変わったら発火する */ });

このイベントをさばくRouterと、さばいた結果動くControllerを用意すれば良いってことですね。

hash駆動なController

そしてざっくり書いたのがこちら。

(function(global) {
    'use strict';

    var Router = function(options) {
        this.controller = options.controller;
        global.addEventListener('hashchange', this);
    };
    Router.prototype = {
        /**
         * あくまで考え方の例で、ほんとはもっとちゃんとする
         * - 今のままでは深い階層のルートに対応してないし
         * - パラメータも1つしか受けてないし
         *
         */
        handleEvent: function(ev) {
            // #!/item/2 の場合
            var hash = ev.newURL.split('#')[1]; // '!/item/2'
            var hashArr = hash.split('/');      // ['!', 'item', '2']
            var routeFuncName = hashArr[1];     // 'item'
            var param = hashArr[2];             // '2'

            // コントローラーにあればそれを
            if (routeFuncName in this.controller) {
                this.controller[routeFuncName](param);
            }
            // なければerrorってやつを
            else {
                this.controller['error']();
            }
        }
    };

    var Controller = function() {};
    Controller.prototype = {
        top: function() {
            console.log('top');
        },
        item: function(itemId) {
            console.log('item', itemId);
        },
        error: function() {
            console.log('error');
        }
    };

    new Router({ controller: new Controller() });
}(window));

コメントにもありますが、いわゆるRouterはもっとちゃんとしてますw

  • historyAPIが動く環境ならそっち使うとか
  • ページがロードされた時に既に"#"が付いてたら動くようにするとか
  • ネストしたルートでも動くとか
  • 引数を個別にいっぱい渡せるとか

大事なのはイメージ!

データを取ってきてViewを組む

これはもはやSPAとか関係ないですね。
上述したControllerのそれぞれの関数内で、必要なデータを取ってきて画面を表示すれば完成。

View用のイケイケなライブラリ使ったり、テンプレートのライブラリ使ったり。
データの処理を分離するためにModel的な概念を用意したりするわけです。

データはサーバーサイドからHTMLに埋めて返すもよし、APIを用意してAjaxするもよし。

これだけ?

そうです、これだけです。
キホンのキとしては本当にこれだけです。

そこに、

  • Googleにインデックスしてほしいとか
  • ローディング表示とか
  • ネストするViewや共通するViewをどうするのとか
  • Ajaxでリクエストしたデータで云々とか
  • 画面スクロール位置とか
  • etc

みたいな混み合った処理が必要になってきて面倒になってくるのがお約束ですが、
それはまぁ追って覚えていけば良いと思います。

というわけでざっくりですが、これであなたも今日からSPA実装経験者です!