backbone.jsでtodoアプリを少しずつ作る(2)
まえおき
前回作ったbackbone.jsのtodoサンプルをより洗練した形に作り変えていきます。
「backbonejsを使ってアプリを作る」から「作りたいアプリのために、backbonejsを使いこなす」ようになるのが最終目標。。。
AppViewのシングルトン化
今回の設計では、アプリ全体を管理するAppViewは常に1つであるべきです。
2回以上newしないように、気をつけてコーディングしてもいいのですが、プログラム側でインスタンスの生成は1度しか行われないようにするのがベストです。
なので、AppViewのconstructorをオーバーライドしてみます。
var AppView = Backbone.View.extend({ ・・・ constructor: function() { if (!AppView.instance) { AppView.instance = this; Backbone.View.apply(AppView.instance, arguments); } return AppView.instance; }, ・・・ });
onはlistenToで代用
FoodViewのinitializeメソッドで、modelに対する変更・削除イベントを購読し、自身のメソッドをコールバックとして割り当てています↓
var FoodView = Backbone.View.extend({ ・・・ initialize: function() { ・・・ this.model.on('change', this.render, this); // ここ this.model.on('destroy', this.remove, this); // ここ ・・・
この場合、Model(発行者)に対して、FoodView(購読者)のメソッドが割り当てられているせいで、ViewをDOMから削除しても、Model側にViewへの参照が残ってしまいます。
結果、削除したViewはガーベジコレクションの対象にならず、この状態で何度もビューを再作成してたりするとメモリリークを起こしてしまいます。
これに対し、listenToを使うとViewがDOMから削除(remove)されたタイミングで、ModelからViewへの参照を切ってくれます。
※listenToで登録した際に、発行側のオブジェクトを保持しておいて、View削除の際に、ちゃんとoff(onの逆メソッド)してくれているようです。
なので、以下のように書き換えます。
var FoodView = Backbone.View.extend({ ・・・ initialize: function() { //修正前 //this.model.on('change', this.render, this); //this.model.on('destroy', this.remove, this); //修正後 this.listenTo(this.model, 'change', this.render); this.listenTo(this.model, 'destroy', this.remove); ・・・
※AppViewのinitializeメソッドは、後程、他の修正と一緒に変更します。
グローバルっぽい変数fcへの依存をなくす
NewViewのaddFoodメソッドの中で、fc.createうんちゃら観たいな記述がありますね↓
var NewView = Backbone.View.extend({ ・・・ addFood: function() { fc.create({name:$(this.el).find('input#newfood-name').val(), ... (省略 // ここ ・・・
この書き方だと、fcが定義されていなかった場合にはエラーを起こしますし、fcという名前が変更になったら、ここも修正が必要になります。
つまり、NewViewはfcに依存したモジュールになってしまっています。
同じことが、FoodListViewにも言えます↓
var FoodListView = Backbone.View.extend({ ・・・ addAll: function() { fc.each(this.add, this); // ここ ・・・
そもそも、なんともグローバルチックな宣言をされているfc...がそもそもいけてない↓
var fc = new FoodCollection;
FoodCollectionは、アプリ全体で扱われるデータなので、アプリ全体を管理するAppViewで一度だけ定義して、変更があってもそこを修正するのみにしたい。
ということで、以下のような感じで修正します。
1. fcけしちゃうっ
//var fc = new FoodCollection;
2. AppViewの初期化時に、FoodCollectionインスタンスを生成し、各Viewに渡す。
var AppView = Backbone.View.extend({ ・・・ collections:{}, // 追加 initialize: function() { this.collections.foods = new FoodCollection; // 追加 this.views.foodlist = new FoodListView({collection:this.collections.foods}); // 変更 this.views.new = new NewView({collection:this.collections.foods}); // 変更 this.collections.foods.fetch({reset:true}); // 変更 // 以下、削除 //fc.on('add', this.views.foodlist.add, this.views.foodlist); //fc.on('reset', this.views.foodlist.addAll, this.views.foodlist); //fc.fetch({reset:true}); ・・・
3.NewView,FoodListViewで、渡されたcollectionを参照するように修正
var NewView = Backbone.View.extend({ ・・・ addFood: function() { ・・・ // 修正前 //fc.create({name:$(this.el).find('input#newfood-name').val(), calory:$(this.el).find('input#newfood-calory').val()}); // 修正後 this.collection.create({name:$(this.el).find('input#newfood-name').val(), calory:$(this.el).find('input#newfood-calory').val()}); ・・・
var FoodListView = Backbone.View.extend({ ・・・ initialize: function() { ・・・ // 追加(AppViewでのonをlistenToに変更してこちらに移動) this.listenTo(this.collection, 'add', this.add); this.listenTo(this.collection, 'reset', this.addAll); }, ・・・ addAll: function() { // 修正前 //fc.each(this.add, this); // 修正後 this.collection.each(this.add, this); ・・・
fcが消えた!すっきり
Modelの変更とDOMの操作は切り離す
NewViewのaddFoodメソッドですが、Foodを追加した後にthis.render()を呼び出しています。
var NewView = Backbone.View.extend({ ・・・ addFood: function() { this.collection.create({name:$(this.el).find('input#newfood-name').val(), calory:$(this.el).find('input#newfood-calory').val()}); this.render(); // ここ ・・・
例えば、ここでさらに、editFoodとか、copyFoodとかいうメソッドを追加したらどうでしょう。
各メソッドの最後にthis.render()を呼び出すでしょうか。。。否、断じて否!
そもそも、Modelの変更とDOMの操作は完全に切り離されていなきゃMVCとは言えません。
という訳で以下のように修正します。
var NewView = Backbone.View.extend({ ・・・ initialize: function() { this.render(); this.listenTo(this.collection, 'add', this.render); // 追加 }, ・・・ addFood: function() { this.collection.create({name:$(this.el).find('input#newfood-name').val(), calory:$(this.el).find('input#newfood-calory').val()}); //this.render(); 削除 ・・・
Collectionに対する変更を監視する形にしてあげれば、editFoodでもcopyFoodでも、最終目的はCollectionの操作までという感じではっきりします。
自身のビューに対する操作でも、きっちりModel,Collectionの修正を監視して操作してあげると、ますます、MVCな感じになります。
完成したサンプル
やってみて
backbonejsは自由度が高い分、設計段階でとても悩みますね。
でもだからこそ、jQuery万歳な人から少しは抜け出せてきた気がする。。。
まあマイペースでがんばるお