Backbone.js でレスポンスをキャッシュするプラクティス

Backbone.js を使ってクライアント MVC で実装したとしても、画面遷移のたびにサーバーと通信していては、データが表示されるまで結局ユーザーは待たされます。操作が固まらない、ただそれだけ。それに何度も通信していては、サーバー側に余計な負担もかかります。取得したデータが頻繁に更新されないものなら、クライアント側でキャッシュしておかないと。


さて、どう実装しましょう。真っ先に思いついたのは、Backbone.Model や Backbone.Collection を継承したクラスに、fetch や save を呼び出してレスポンスをキャッシュするメソッドを追加する方法。シンプルですけど、キャッシュを扱うコードが分散してしまうのが難点かな。


Backbone.Model や Backbone.Collection が通信を行うとき、Backbone.sync メソッドを必ず経由します。そうだ、Backbone.sync を上書きすればいいかも。この方法ならコードが分散しなくて済みます。ベストじゃないけど、まぁ悪くなさそうです。いざ実行。

<!DOCTYPE html>
<html>
    <head>
        <title>BackboneSyncSample</title>
    </head>
    <body>
        <h1>BackboneSyncSample</h1>
        <div id="main"></div>

        <script id="entry-template" type="text/template">
            <h2><%- title %></h2>
            <div><%- content %></div>
        </script>

        <script type="text/javascript" src="jquery-1.6.4.js"></script>
        <script type="text/javascript" src="underscore.js"></script>
        <script type="text/javascript" src="backbone.js"></script>
        <script type="text/javascript" src="jquery.mockjax.js"></script>
        <script type="text/javascript">
            var Entry = Backbone.Model.extend({
                defaults: {
                    title: "",
                    content: ""
                }
            });

            var Blog = Backbone.Collection.extend({
                model: Entry,
                url: "/blog/"
            });

            var EntryView = Backbone.View.extend({
                template: _.template($("#entry-template").html()),
                render: function() {
                    var json = this.model.toJSON();
                    var html = this.template(json);
                    $(this.el).html(html);
                    return this;
                }
            });

            var BlogView = Backbone.View.extend({
                el: $("#main"),
                render: function() {
                    $(this.el).empty();
                    this.model.each(function(entry) {
                        var view = new EntryView({ model:entry });
                        view.render();
                        $(this.el).append(view.el);
                    }, this);
                    return this;
                }
            });

            // API 用意するの面倒なので mockjax で代用
            $.mockjax({
                url: "/blog/",
                responseText: [
                    { title:"foo", content:"bar" },
                    { title:"hoge", content:"fuga" }
                ]
            });

            // オリジナルの sync をコピー
            var originalSync = Backbone.sync;
            // Backbone.sync をオーバライド
            Backbone.sync = function(method, model, options) {
                if (method === "read") {
                    // 通信に成功したとき用のコールバック関数をコピー
                    var originalSuccess = options.success;

                    // ローカルストレージからキャッシュを取得
                    // キーは URL
                    var cache = localStorage.getItem(model.url);
                    if (cache) {
                        // キャッシュが存在したら成功用コールバックを呼び出す
                        originalSuccess(JSON.parse(cache));
                    } else {
                        // レスポンスをローカルストレージに保存するように、
                        // 成功用コールバックを上書き
                        options.success = function(collection) {
                            localStorage.setItem(model.url, JSON.stringify(collection));
                            originalSuccess(collection);
                        };
                        // キャッシュが存在しなかったらオリジナルの sync を呼び出す
                        originalSync(method, model, options);
                    }
                } else {
                    // read 以外はオリジナルの sync で処理する
                    originalSync(method, model, options);
                }
            };

            window.App = new BlogView({ model: new Blog() });
            window.App.model.fetch({
                success: function(collection, resp) {
                    window.App.render();
                }
            });
        </script>
    </body>
</html>

今回はローカルストレージにキャッシュしてみました。FirebugChromeデベロッパーツールでステップ実行すると、最初だけ通信し、それ以降はローカルストレージから取得していることが確認できると思います。ローカルストレージに保存しているので、ページを更新しても通信しません。


そうそう、Backbone.js のリポジトリにあるサンプルには、ローカルストレージを簡単に操作するライブラリが含まれていました。backbone-localstorage.js ってやつ。ただ、これ使うとローカルストレージにしかアクセスできません。惜しい。キャッシュが無いときはサーバーに取りに行って欲しかったので、今回は採用を見送りました。