クライアントサイドMVCフレームワークをもっと勉強したい。
今僕は#Favtileというサービスを作っています。
Twitterのお気に入り機能のためのツールです。
そこではクライアントサイド・サーバーサイド共にCoffeeScriptで書いており、Spine.jsを使っています。
Spine.jsを使いはじめた理由? CoffeeScriptが好きだからです。Spine.jsはCoffeeScriptで書かれていますから。
でも他にもJavaScriptのMVCフレームワークはいっぱいあります。
有名だと思うのでBackbone.jsをやります。
有名だと情報が多いので無名のフレームワークよりも勉強しやすいという考えです。
Backbone.jsのサンプルのTodoアプリを読みながらドキュメントを引いて調べていきます。
Backbone Demo: Todos
Todo Model
Todoモデルはtext、order、doneの3つの属性を持つようです。
属性のデフォルト値がある場合はdefaultsに指定する。
toggleはTodoが完了した/してないを切り替えるメソッドですね。
textの属性持つとコメントには書いてるけどModelの中には「text」が見当たらない。そんなものか。
Todo
window.Todo = Backbone.Model.extend({
todo
defaults: function() {
return {
done: false,
order: Todos.nextOrder()
};
},
todo
toggle: function() {
this.save({done: !this.get("done")});
}
Backbone.jsのドキュメントを読みながらもっと詳しく読む。
defaults
Modelをインスタンス化するときに属性の値が指定されてなかった時はデフォルト値としてdefaultsの内容が使われる。
ここでdefaultsに関数を指定するとその関数の返り値をデフォルト値として使う、と。
デフォルト値が毎回変わるような場合に関数を指定するといい感じなのかなぁ。
save
Modelのsaveメソッドは、内部でBackbone.syncを使っていて、Backbone.syncではRESTでサーバーにアクセスする。
LocalStorage adapterを使ってるとここが置き換わるかんじか。
saveメソッドに変更したい属性達を{}でまとめて渡すか、save("key", "value")のような感じで呼び出して使う、と。
validateメソッドを実装してvalidateとおらないと保存出来ないようにすることも出来るみたい。
isNewっていうので新規作成されたものか判定してるのかな? 新しく作成されるときはcreateがよばれ、更新の時はupdateが呼ばれるよう。
saveには{success:成功時の処理, error:失敗時の処理}のような感じで成功時・失敗時のコールバック関数を渡せるらしい。
Todo Collection
modelプロパティにCollectionが持ってるモデルのクラスを指定する、Collection.addとかcreateするときにmodelを使うのかなぁ。
Storeっていうのはローカルストレージに読み書きするためのクラスかな?
collectionの中からdoneなやつだけ取るのがdoneメソッドで、remainingはコレクションからdoneなやつを除いたもの、か。読みやすい。
nextOrderでTodoの順番を作ってる。
comparetorプロパティを定義するとcollectionを並べるときにその値が使われるようです。
今のだと追加した順に並ぶようになってますね。
Todo
window.TodoList = Backbone.Collection.extend({
model: Todo,
todo
localStorage: new Store("todos"),
todo
done: function() {
return this.filter(function(todo){ return todo.get('done'); });
},
todo
remaining: function() {
return this.without.apply(this, this.done());
},
nextOrder: function() {
if (!this.length) return 1;
return this.last().get('order') + 1;
},
comparator: function(todo) {
return todo.get('order');
}
});
window.Todos = new TodoList;
TodoView
SpineでいうとControllerにあたる部分。
Backbone.Viewを継承してる。
はtagName、className、id、attributesを指定することで表示用のエレメントが作られ、elプロパティに保持されるようで。
_.template便利だなぁ。クライアントサイドのテンプレートっていっぱいあるのね…
eventsっていうプロパティに{"eventName selector":"methodName"}の形式で書くっぽい。
内部的にはjQueryのdelegateが呼ばれてイベントが設定される。(設定するときには名前空間もくっつけてるみたい)
clickしたときはtoggleDoneがよばれて、その中でmodelのtoggleが呼ばれてdoneの値が更新されて、changeイベントが発行されてレンダリングし直されるのかな。
dbclickではeditが呼ばれて、その中で編集中の見た目になるようクラスを追加して、入力欄にフォーカス当ててるみたい。
ではフォーカスはずれた時はどうなるかというと
renderの中でsetTextをよんでいて、その中で、フォーカスが外れたとき(blur)はcloseを呼ぶように設定してる。
closeの中ではモデルの値を保存したあと、編集中の見た目を取り除いてる感じ。
enterが押された時にもcloseは呼ばれるみたいだ。なるほど。
initializeが初期化する時によばれるものかな?
- モデルが変更されたらレンダリングしなおす
- モデルが捨てられたら要素を取り除く
みたいな設定をしてるみたい。
window.TodoView = Backbone.View.extend({
//... is a list tag.
tagName: "li",
// Cache the template function for a single item.
template: _.template($('#item-template').html()),
// The DOM events specific to an item.
events: {
"click .check" : "toggleDone",
"dblclick div.todo-text" : "edit",
"click span.todo-destroy" : "clear",
"keypress .todo-input" : "updateOnEnter"
},
// The TodoView listens for changes to its model, re-rendering.
initialize: function() {
this.model.bind('change', this.render, this);
this.model.bind('destroy', this.remove, this);
},
// Re-render the contents of the todo item.
render: function() {
$(this.el).html(this.template(this.model.toJSON()));
this.setText();
return this;
},
// To avoid XSS (not that it would be harmful in this particular app),
// we use `jQuery.text` to set the contents of the todo item.
setText: function() {
var text = this.model.get('text');
this.$('.todo-text').text(text);
this.input = this.$('.todo-input');
this.input.bind('blur', _.bind(this.close, this)).val(text);
},
// Toggle the `"done"` state of the model.
toggleDone: function() {
this.model.toggle();
},
// Switch this view into `"editing"` mode, displaying the input field.
edit: function() {
$(this.el).addClass("editing");
this.input.focus();
},
// Close the `"editing"` mode, saving changes to the todo.
close: function() {
this.model.save({text: this.input.val()});
$(this.el).removeClass("editing");
},
// If you hit `enter`, we're through editing the item.
updateOnEnter: function(e) {
if (e.keyCode == 13) this.close();
},
// Remove this view from the DOM.
remove: function() {
$(this.el).remove();
},
// Remove the item, destroy the model.
clear: function() {
this.model.destroy();
}
});
AppView
なるほど。既にある要素をelにバインドする事も出来る、と。
addとかresetとかそこらへんのイベントはFAQの中にCatalog of Eventsで載ってる。
window.AppView = Backbone.View.extend({
el: $("#todoapp"),
statsTemplate: _.template($('#stats-template').html()),
events: {
"keypress #new-todo": "createOnEnter",
"keyup #new-todo": "showTooltip",
"click .todo-clear a": "clearCompleted"
},
initialize: function() {
this.input = this.$("#new-todo");
Todos.bind('add', this.addOne, this);
Todos.bind('reset', this.addAll, this);
Todos.bind('all', this.render, this);
Todos.fetch();
},
render: function() {
this.$('#todo-stats').html(this.statsTemplate({
total: Todos.length,
done: Todos.done().length,
remaining: Todos.remaining().length
}));
},
todo
addOne: function(todo) {
var view = new TodoView({model: todo});
$("#todo-list").append(view.render().el);
},
addAll: function() {
Todos.each(this.addOne);
},
Todo
createOnEnter: function(e) {
var text = this.input.val();
if (!text || e.keyCode != 13) return;
Todos.create({text: text});
this.input.val('');
},
todo
clearCompleted: function() {
_.each(Todos.done(), function(todo){ todo.destroy(); });
return false;
},
todo
showTooltip: function(e) {
var tooltip = this.$(".ui-tooltip-top");
var val = this.input.val();
tooltip.fadeOut();
if (this.tooltipTimeout) clearTimeout(this.tooltipTimeout);
if (val == '' || val == this.input.attr('placeholder')) return;
var show = function(){ tooltip.show().fadeIn(); };
this.tooltipTimeout = _.delay(show, 1000);
}
});
window.App = new AppView;
});
まとめ
だらだらと書いた割に内容のない文章になってしまった。
実際に自分で何か作ってみる方が理解が進むかも。(あるいはBackbone.jsのソースを読むとか)