試行錯誤の末にたどり着いた Backbone.Router の使い方

だいぶ前に書いた Backbone.js の入門記事で Backbone.Router を使ったサンプルを紹介した。

Router が表示する View を切り替える良い方法が思いつかなかったので、サンプルではメイン View をグローバル変数に格納して Router から触れるようにし、無理やり切り替えていた。今見ても、全然スマートな方法じゃないね。

あれから試行錯誤しながら、何個か Backbone.js を使ってアプリを作成してみて、ようやく自分の中で Backbone.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() {
                    Backbone.history.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;
                },

                // ブログタイトル(?) がクリックされたらトップページに移動する。
                showIndex: function() {
                    Backbone.history.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"
                },

                // ルーターの初期化。
                // ビューとモデルはメンバとして保持する。
                initialize: function() {
                    this.blog = new Blog();

                    // テストデータを作成
                    this.blog.add(new Entry({ id: 1, title: "foo", content: "hoge" }));
                    this.blog.add(new Entry({ id: 2, title: "bar", content: "fuga" }));

                    this.appView = new BlogView({ model: this.blog });
                    this.appView.render();
                },

                list: function() {
                    this.appView.render();
                },

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

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

Router がメンバに View と Model(と Collection)を保持し、ルーティングでは、メンバに保持している View や Model を使って表示を切り替えている。この方法は backbone-rails のジェネレータが生成する Router が使っている方法で、CoffeeScript だったけど参考になった。

Router 経由でビューを切り替えたい場合、以前はグローバル変数に格納した Router を使っていたけど、Backbone.history.navigate を使うようにしている。Router の navigate は Backbone.history.navigate に委譲しているだけなので、わざわざ Router をグローバル変数に格納する必要は無かった。