とある角度から

お腹いっぱいたべられる幸せ

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;
  },
         ・・・
});

これで2回目以降のインスタンス生成の際は、最初に作成したインスタンスを参照するようになります。

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万歳な人から少しは抜け出せてきた気がする。。。
まあマイペースでがんばるお