作って覚える Backbone.js (2) Router 編

モバイル版 Basecamp に使われていると聞いて興味を抱いていた、JavaScriptMVC フレームワーク Backbone.js。Model と View は先日試しました。

でも、Controller はまだ。「今度試す」といってやらないことが多い私ですけど、Controller は自分も使うので、ちゃんと行動に移すことにします。


Backbone では Controller の役割は Backbone.Router を継承したクラスが行います。Routerの定義の仕方はこんな感じ。

var AppRouter = Backbone.Router.extend({
    routes: {
        "post/": "list",
        "post/:id": "show"
    },

    list: function() {
        # データ一覧を表示
    },

    show: function(id) {
        # データの詳細表示
    }
});

extend メソッドを使うのは Model や View と同じです。
注目は、routes で URL パターンとメソッドをマッピングしているところ。Router のメソッドがリクエストハンドラみたいな役割をします。大したことはできませんけどね。
URL パターンに : をつけた部分があると、その部分がマッピングしたメソッドに引数で渡されます。上記の例だと、:id の部分が show メソッドの引数 id に渡される、という動きになります。URL パターンの書き方が Rails っぽい。


あとは

window.router = new AppRouter();
Backbone.history.start({ pushState: true });

とすれば、これで Router が作用するようになります。Backbone.history.start で履歴監視も開始しているので、ブラウザの戻る・進むにも対応できて、めでたしめでたし。


上記の説明だけでは紙面が寂しいので、Router を使ったサンプルを作ってみました。

<!DOCTYPE html>
<html lang="ja">
    <head>
        <meta charset="utf-8">
        <title>Backbone Router Sample</title>
    </head>
    <body>
        <div id="main">
            <h1><a href="">Backbone Router Sample</a></h1>
            <div id="entries">
            </div>
        </div>

        <!--エントリをリスト表示するとき使うテンプレート-->
        <script id="entry-template" type="text/template">
            <div class="entry">
                <h3><a href="javascript:void(0)"><%- title %></a></h3>
            </div>
        </script>

        <!--エントリの詳細を表示するとき使うテンプレート-->
        <script id="detail-template" type="text/template">
            <h2><%- title %></h2>
            <div id="content">
                <%- content %>
            </div>
        </script>

        <script type="text/javascript" src="http://ajax.googleapis.com/ajax/libs/jquery/1.6.4/jquery.min.js"></script>
        <script type="text/javascript" src="http://documentcloud.github.com/underscore/underscore-min.js"></script>
        <script type="text/javascript" src="http://documentcloud.github.com/backbone/backbone-min.js"></script>
        <script type="text/javascript">
            // エントリ。
            var Entry = Backbone.Model.extend({
                defaults: {
                    title: "",
                    content: ""
                }
            });

            // ブログ。
            var Blog = Backbone.Collection.extend({
                model: Entry
            });

            // エントリを表示するビュー。
            var EntryView = Backbone.View.extend({
                template: _.template($("#entry-template").html()),

                events: {
                    "click a": "showDetail",
                },

                render: function() {
                    $(this.el).html(this.template(this.model.toJSON()));
                    return this;
                },

                // エントリタイトルをクリックしたら Router 経由で
                // 詳細ページに移動する
                showDetail: function() {
                    window.router.navigate("entry/" + this.model.id, true);
                    return false;
                }
            });

            // エントリの詳細を表示するビュー。
            var DetailView = Backbone.View.extend({
                template: _.template($("#detail-template").html()),
                
                render: function() {
                    $(this.el).html(this.template(this.model.toJSON()));
                    return this;
                }
            });

            // メインのビュー。
            // エントリの一覧を表示する。
            var BlogView = Backbone.View.extend({
                el: $("#main"),
               
                events: {
                    "click h1>a": "showIndex"
                },

                render: function() {
                    var entriesEl = $(this.el).find("#entries");
                    $(entriesEl).empty();

                    this.model.each(function(entry) {
                        var view = new EntryView({ model: entry });
                        view.render();
                        $(entriesEl).append(view.el);
                    }, this);
                    return this;
                },

                // ブログタイトル(?) がクリックされたら Router 経由で
                // トップページを表示する。
                showIndex: function() {
                    window.router.navigate("", true);
                    return false;
                },

                // 渡された ID に該当するエントリの詳細を表示。
                showDetail: function(id) {
                    var entry = this.model.get(parseInt(id));
                    var view = new DetailView({ model: entry });
                    view.render();
                    
                    var entriesEl = $(this.el).find("#entries");    
                    $(entriesEl).empty();
                    $(entriesEl).append(view.el);
                }
            });

            // ルーター。
            var AppRouter = Backbone.Router.extend({
                // routes でルーターのメソッドと URL パターンをマッピングする。
                // ルーターのメソッドがリクエストハンドラになる。
                routes: {
                    "entry/:id": "show",
                    "entry/": "list",
                    "": "list"
                },

                list: function() {
                    window.App.render();
                },

                show: function(id) {
                    window.App.showDetail(id);
                }
            });
            window.router = new AppRouter();

            var blog = new Blog();
            blog.add(new Entry({ id: 1, title: "foo", content: "hoge" }));
            blog.add(new Entry({ id: 2, title: "bar", content: "fuga" }));
            window.App = new BlogView({ model: blog });
            window.App.render();

            // 履歴監視スタート。
            // Backbone.history.start({ pushState: true }); で pushState は有効にできるけど、
            // Web サーバーでホストしないとうまく動かないので無効にしておく。
            Backbone.history.start();
        </script>
    </body>
</html>

エディタにコピペして UTF-8 で保存し、ブラウザで表示してみてください。リンクをクリックすると非同期で画面遷移するし、直接 URL 入力すると該当画面がちゃんと表示されるはずです。


上記のサンプルでは、Router オブジェクトをグローバル変数に格納して、View 内で画面遷移するときに使っています。このやり方がしっくりきていません。ネットでも Router を使ったサンプルは見つからなかったし…。もっとスマートなやり方がありそうなんですけどね。