モバイル版 Basecamp に使われていると聞いて興味を抱いていた、JavaScript の MVC フレームワーク 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 を使ったサンプルは見つからなかったし…。もっとスマートなやり方がありそうなんですけどね。