Backbone入門 その4
おはようございます、south37です。
今日はBackbone入門最終回です。
最後は、Routerについて勉強したいと思います。例によって、この連載を参考にしています。
Routerとは
Backbone.jsのRouterは、クライアントサイドでルーティング機能を実現する為のオブジェクトです。通常、ブラウザで特定のURLにアクセスするとそのページを構成するhtmlやjs、cssがブラウザに送られ、ブラウザはそれらをレンダリングする事でWebページを表示します。動的なWebページであれば、サーバ側で様々な処理が施された後で、レンダリングされたテンプレートファイルがhtmlの形で送られる事になります。
サーバにおけるルーティングとは、アクセスされたURLとサーバ側の処理を対応づける事です。では、クライアントサイドで動くBackbone.jsのルーティングとはどういう事でしょうか?
実は、HTML5ではhistoryAPIという機能のおかげで、URLとJSのメソッドを関連づける事が出来ます。Backboneではこれを利用して、JSで動的にページを書き換える事で、サーバにアクセスしない画面遷移を実現する事が出来ます。これがクライアントサイドでのルーティングです。
では、実際にコードを見てみましょう。
//router.js var app = {}; var AppRouter = Backbone.Router.extend({ routes: { "": "index", "create": "create", "edit/:id": "edit" }, initialize: function () { this.collection = new MemoList(); this.headerView = new HeaderView({el:$("#header")}); this.editView = new EditView({el:$("#editForm"), collection:this.collection}); this.listView = new ListView({el:$("#memoList"), collection:this.collection}); }, index: function(){ this.editView.hideView(); }, create: function () { this.editView.model = new Memo(null, {collection: this.collection}); this.editView.render(); }, edit: function (id) { this.editView.model = this.collection.get(id); if (this.editView.model) { this.editView.render(); } } }); app.router = new AppRouter(); Backbone.history.start({pushState: true});
上記のroute.jsでは、ルーティング用のオブジェクトであるAppRouterを定義しています。routes属性の中で、URLとメソッド名の対応付けが行われています。"edit/:id"のようなパラメータを渡すURL指定も可能です。
各々のメソッドの中では、viewのレンダリングが行われています。これによって、URLに対してサーバーサイドで動的にページを書き換える事が出来る訳です。
initializeの中では、レンダリング用のviewオブジェクトの生成が行われています。indexではeditViewを消し、createでは逆にeditViewをrender()しています。また、editではidを受け取って、一度作られたメモの編集を実現しています。
上記の"", "create", "edit/:id"等はrouteイベントと呼ばれ、Backbone.Routerのnavigateメソッドを使う事で発火する事が出来ます。
var HeaderView = Backbone.View.extend({ events:{ "click #create": "onCreate" }, initialize: function () { _.bindAll(this, "onCreate"); }, onCreate: function () { app.router.navigate("create", {trigger:true}); } });
上記の様に、app.router.navigateで第一引数に渡したイベントが発生します。ただし、このときはoptionsとして{trigger: true}を渡してやる必要があります。
"edit/:id"のような形のイベントを発行したい時は、単純に
app.router.navigate("edit/" + this.model.cid, {trigger: true});
といった形で文字列連結してやればOKです。
Backbone.Routerを使う事で、サーバとはバックグラウンドでのみ通信し、ページ遷移を極めて高速に行うアプリケーションを作る事が出来ます。
追記
例によって、手元で書いたコードを載せときます。連載のものとほとんど変わらないですが、サーバとの通信が不要になるように少し書き換えてあります。
model.js
var Memo = Backbone.Model.extend({ defaults: { "title": "", "content": "" }, validate:function (attributes) { if (attributes.content === "") { return "content must be not empty."; } } }); var MemoList = Backbone.Collection.extend({ model:Memo, url:"/memos" });
view.js
var HeaderView = Backbone.View.extend({ events:{ "click #create": "onCreate" }, initialize: function () { _.bindAll(this, "onCreate"); }, onCreate: function () { app.router.navigate("create", {trigger:true}); } }); var EditView = Backbone.View.extend({ events: { "click #saveBtn": "onSave" }, initialize: function () { _.bindAll(this, "render", "onSave", "hideView"); this.$title = $("#editForm [name='title']"); this.$content = $("#editForm [name='content']"); }, render: function () { this.$title.val(this.model.get("title")); this.$content.val(this.model.get("content")); this.$el.show(); }, onSave: function () { this.model.set({ title: this.$title.val(), content: this.$content.val() }); this.collection.add(this.model, {merge: true}); this.hideView(); }, hideView: function () { this.$el.hide(); app.router.navigate("", {trigger: true}); } }); var ItemView = Backbone.View.extend({ tmpl: _.template($("#tmpl-itemview").html()), events: { "click .edit": "onEdit", "click .delete": "onDelete" }, initialize: function () { _.bindAll(this, "onEdit", "onDelete", "render"); this.listenTo(this.model, "change", this.render); }, onEdit: function () { app.router.navigate("edit/" + this.model.cid, {trigger: true}); }, onDelete: function () { this.model.destroy(); this.remove(); }, render: function () { this.$el.html(this.tmpl(this.model.toJSON())); return this; } }); var ListView = Backbone.View.extend({ initialize: function () { _.bindAll(this, "render", "addItemView"); this.listenTo(this.collection, "add", this.addItemView); this.render(); }, render: function () { this.collection.each(function (item) { this.addItemView(item); }, this); }, addItemView: function (item) { this.$el.append((new ItemView({model: item})).render().el); } });
router.js
var app = {}; var AppRouter = Backbone.Router.extend({ routes: { "": "index", "create": "create", "edit/:id": "edit" }, initialize: function () { _.bindAll(this, "index", "create", "edit"); this.collection = new MemoList(); this.headerView = new HeaderView({el:$("#header")}); this.editView = new EditView({el:$("#editForm"), collection:this.collection}); this.listView = new ListView({el:$("#memoList"), collection:this.collection}); }, index: function(){ this.editView.hideView(); }, create: function () { this.editView.model = new Memo(null, {collection: this.collection}); this.editView.render(); }, edit: function (id) { this.editView.model = this.collection.get(id); if (this.editView.model) { this.editView.render(); } } }); app.router = new AppRouter(); Backbone.history.start();
index.html
<!DOCTYPE html> <html> <head> <meta charset="utf-8"> <title>Backbone Router Example</title> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <link href="lib/bower_components/bootstrap/dist/css/bootstrap.min.css" rel="stylesheet"> <style> body { padding-top: 60px; /* 60px to make the container go all the way to the bottom of the topbar */ } </style> </head> <body> <div id="header" class="navbar navbar-inverse navbar-fixed-top" role="navigation"> <a href="./index.html" class="navbar-brand">Backbone Memo Application</a> <a id="create" class="btn btn-primary navbar-btn navbar-right">Create</a> </div> <div id="main" class="container"> <h1>Memo</h1> <form id="editForm" role="form" class="form" style="display: none"> <div class="form-group"> <input type="title" name="title" class="form-control" placeholder="Title"/> </div> <div class="form-group"> <textarea rows="5" name="content" class="form-control" placeholder="Content"></textarea> </div> <a href="#" id="saveBtn" class="btn btn-primary">Save</a> <a href="#" id="cancelBtn" class="btn btn-danger">Cancel</a> </form> <div id="memoList" class="row-fluid"> </div> </div> <script type="text/template" id="tmpl-itemview"> <h3><%= title %></h3> <p><%= content %></p> <p><a class="edit">edit</a> <a class="delete">delete</a></p> </script> <script src="lib/bower_components/jquery/jquery.min.js"></script> <script src="lib/bower_components/bootstrap/dist/js/bootstrap.min.js"></script> <script src="lib/bower_components/json2/json2.js"></script> <script src="lib/bower_components/underscore/underscore-min.js"></script> <script src="lib/bower_components/backbone/backbone-min.js"></script> <script src="lib/js/model.js"></script> <script src="lib/js/view.js"></script> <script src="lib/js/router.js"></script> </body> </html>
ちなみに、router.jsの最後でBackbone.history.start()としており、{pushState: true}を渡していません。その為、historyAPIを使わず、hash fragment(#~というヤツ)を用いたURLで状態を表す形式になっています*1。
*1:手元のPCでhistoryAPIを使った形式を試していると、何故か"insecure ~ "のようなエラーが出てしまったんですよね....。原因が分からんくて困っています