一から勉強させてください( ̄ω ̄;)

最下級エンジニアが日々の学びをアウトプットしていくだけのブログです。

backbone.jsのサンプルTodosの動作を追ってみた

今回は、ついにver1.0.0になったbackbone.jsを勉強しようということでサンプルとして紹介されているTodosを真似してつくってみました。

ちなみにこちらが見本のやつです↓
http://backbonejs.org/docs/todos.html


ついでに今回ぼくが真似して書いたやつがこれです↓
https://github.com/dangerousanimal/Todos

Todos.jsはTodo, TodoList, TodoView, AppViewの4つのクラスから構成されていて、
TodoがBackboneのModel、TodoListがCollection、TodoView, AppViewがViewをそれぞれ拡張して作られています。

ざっくり言うと、これらが良い感じに絡み合ってうまく動作しているんですね。


では復習がてらTodos.jsの動作を順に追っていきたいと思います!!


①Todo (Model)

//Todo Model
   var Todo = Backbone.Model.extend({

      defaults : function(){
        return {
          title : "empty todo...OTL",
          order : Todos.nextOrder(),
          done : false
        };
      },

      toggle : function(){
        this.save({done : !this.get("done")});
      }

    });


まずTodoからです。
上記のようにBackbone.Model.extendとすることでBackboneのModelを継承して拡張できるみたいです。


そしてここで定義しているdefaultsはもとのBackbone.jsでは空で設定されており、
「自分で勝手にデフォルトの値を設定してください」みたいな感じになってます。


今回はtitleにTodoの内容、doneがタスクを完了したかどうかを判別するフラグになっています。

で、toggleメソッドを用意して、これによってdoneのtrue, falseを切り換えるようにしています。
例えば、現在のdoneがfalseならtrueに切り換わります。


②TodoList (Collection)

 //Todo Collection
    var TodoList = Backbone.Collection.extend({
      
      model : Todo,

      //名前をtodos-backboneとして、そこに登録したtodoを保存
      localStorage : new Backbone.LocalStorage("todos-backbone"),

      done : function(){
        //done:trueのModelを返す
        return this.where({done : true}); 
      },

      remaining : function(){
        //withoutはunderscoreの関数 948行目でCollectionでも使えるようにしてる
        //done:trueのもの以外を返す
        return this.without.apply(this, this.done()); 
      },

      nextOrder : function(){
        if(!this.length) return 1;
        //lastはunderscoreの関数
        return this.last().get("order") + 1;
      },

      comparator : "order"

    });

    var Todos = new TodoList;

次にTodoListです。
Collectionは複数のModelをまとめて扱うためのものなので、まず対象となるModel(今回はTodo)を指定します。


localStorageはそのままでlocalstorageに現在のタスクを保存しておくための機能です。


doneは全Modelのうちでdone:trueのものを返すメソッド、remainingはdone:trueではないものを返すメソッドとなっています。

あ、remainingで使われているwithoutはunderscore.jsで定義されているメソッドで、backboneのcollectionでもこのメソッドを使えるようにしているっぽかったです。


で、このクラスのインスタンスをTodosに入れて準備完了です。


③TodoView

//Todo View
    var TodoView = Backbone.View.extend({
      //デフォルトのtagNameはdiv
      tagName : "li",
      //underscoreのテンプレート
      template : _.template($("#item-template").html()),

      events: {
        "click .toggle"   : "toggleDone",
        "dblclick .view"  : "edit",
        "click a.destroy" : "clear",
        "keypress .edit"  : "updateOnEnter",
        "blur .edit"      : "close"
      },

      initialize: function() {
        this.listenTo(this.model, 'change', this.render);
        this.listenTo(this.model, 'destroy', this.remove);
      },

      render : function(){
        this.$el.html(this.template(this.model.toJSON()));
        //modelのdoneがtrueならdoneクラス追加、falseなら削除
        this.$el.toggleClass("done", this.model.get("done")); 
        //this.inputというプロパティをつくって、それにthis.$(".edit")をキャッシュしてるだけ?
        this.input = this.$(".edit");
        return this;
      },

      toggleDone : function(){
        this.model.toggle();
      },

      edit : function(){
        this.$el.addClass("editing");
        this.input.focus();
      },

      close : function(){
        var value = this.input.val();
        if(!value){
          this.clear();
        }else{
          this.model.save({title : value});
          this.$el.removeClass("editing");
        }
      },

      updateOnEnter : function(e){
        if(e.keyCode == 13) this.close();
      },

      clear : function(){
        this.model.destroy();
      }

    });

次にTodoViewです。

まずtagNameで新しく作成する要素のタグを指定します。
今回はulの中に追加していくので、liとしています。(デフォルトはdivになってます。)


templateはunderscoreのtemplateを使用しています。こいつにModelのデータを渡してレンダリングするんですね。
templateはこんな感じです。

  <script type="text/template" id="item-template">
    <div class="view">
      <input class="toggle" type="checkbox" <%= done ? 'checked="checked"' : '' %> />
      <label><%- title %></label>
      <a class="destroy"></a>
    </div>
    <input class="edit" type="text" value="<%- title %>" />
  </script>


ちなみにunderscoreのテンプレは<% 〜 %>:Javascriptとして評価することができる(ifとかforとか)<%= 〜 %>: テンプレに渡したデータの中身を出力することができる<%- 〜 %>:  テンプレに渡したデータの中身をエスケープして出力することができる

て感じで使います。


eventsではDOMイベントが発生した際に呼び出すメソッドを定義してあります。
{"event selector" : "callback"} としておくことで特定の要素に対して定義する事ができます。
(要素を指定しなかったらそのViewのelに対するイベントになります)

今回だと、.viewをdblclickしたらその中身を編集でき、Enterキー押下またはblurで編集完了みたいな具合ですね。


initializeではlistenToというメソッドが多用されていますが、これはBackboneのオブザーバパターンを実現するためのメソッドです。
(onというメソッドもあるんですが、メモリリークなどの問題等が懸念されることから現在はlistenToが推奨されているみたいです)

Backboneではオブジェクト間を疎結合に保つために、購読者と発行者と呼ばれるオブジェクトがうまく絡み合って動作します。
発行者はイベントが発生したらそのイベントを購読しているすべての購読者に通知する仕組みとなっているんですね。

だから今回の場合だと、「Model側でchangeイベントが発行されたら、TodoView側でrenderを実行します!」みたいにあらかじめ、どのイベントを購読するかを指定しているんですね。


renderはtemplateのところでちらっと触れましたがModelのデータ(toJSONで)を渡してレンダリングをするメソッドです。


toggleDoneは先ほどModelで設定したtoggleメソッドが実行されるところです。


clearはModelのdestroy(削除)をします。
そしてModelのdestroyが実行されると同時にdestroyイベントが発行されるので、TodoView側でもthis.removeが実行されるという仕組みになっています。removeでDOMの削除と stopListening(購読の中止)が行われます。
このstopListeningが先ほど挙げたメモリリーク対策につながるんですね。


④AppView

//Application View
    var AppView = Backbone.View.extend({
      
      el : $("#todoapp"),

      statsTemplate : _.template($("#stats-template").html()),

      events : {
        "keypress #new-todo" : "createOnEnter",
        "click #clear-completed" : "clearCompleted",
        "click #toggle-all" : "toggleAllComplete"
      },

      initialize : function(){
        this.input = this.$("#new-todo");
        this.allCheckbox = this.$("#toggle-all")[0];

        this.listenTo(Todos, "add", this.addOne);
        this.listenTo(Todos, "reset", this.addAll);
        //all→すべてのイベント購読
        this.listenTo(Todos, "all", this.render);
        this.footer = this.$("footer");
        this.main = this.$("#main");

        Todos.fetch();
      },

      render : function(){
        var done = Todos.done().length;
        var remaining = Todos.remaining().length;

        if(Todos.length){
          this.main.show();
          this.footer.show();
          this.footer.html(this.statsTemplate({done : done, remaining : remaining}));
        }else{
          this.main.hide();
          this.footer.hide();
        }
        //終わっていないタスクがひとつでもあればallCheckboxはfalse
        this.allCheckbox.checked = !remaining;

      },

      addOne : function(todo){
        var view = new TodoView({model : todo});
        this.$("#todo-list").append(view.render().el);
      },

      addAll : function(){
        Todos.each(this.addOne, this);
      },

      createOnEnter : function(e){
        if(e.keyCode != 13) return;
        if(!this.input.val()) return;

        Todos.create({title : this.input.val()});
        this.input.val("");
      },

      clearCompleted : function(){
        _.invoke(Todos.done(), "destroy");
        return false;
      },

      toggleAllComplete : function(){
        var done = this.allCheckbox.checked;
        Todos.each(function(todo){
          todo.save({"done" : done});
        });
      }

    });

    var App = new AppView;


最後にAppViewです。

まず、var App = new AppViewとしてインスタンスを生成することでアプリケーションが起動します。

そして、initializeでTodos.fetch()を実行していますが、これによって初期データを読み込みます。

そしてこの際、呼び出された各Modelに対してaddイベントが発行されるので、個々のModelがaddOneされ、その結果すべてのTodoが表示されます。addOneで先ほど書いたTodoViewのインスタンスを生成し、レンダリングしてます。

(これってCollectionでfetchしたときにresetイベント発行→addAllでしたっけ??ちょっとあいまいなので、また調査します)


次にeventsのところで

"keypress #new-todo" : "createOnEnter"

という部分があると思いますが、ここが新たにTodoを追加する部分です。

inputを入力した状態でEnterキーを押すと、inputに入力した内容が新たなModelのtitleとなり、
それをcreateで新たにCollectionに追加するんですね。

そして追加されるということはおなじみのaddイベントが発行され、その結果addOneが実行され、レンダリング完了となります。


以上、割愛した部分もかなり多いですが、ざっとTodosの動作をみてみました。
まだ理解しきれていないとこもありますが、何となくでもBackbone.jsの凄さが分かった気がします。
コードがきれい!管理しやすい!


ただあくまで今回はサンプルをまるパクリして、動作を追ってみただけなので、
今後は自分でアプリケーションを作れるように頑張らんといかんです。

がんばるぞー


小さな事からコツコツと。