Backbone䜿っおみるメモ。ただあんたりよくわかっおないんだなヌ。

よくわからないならコヌドを読めばいいじゃない、ずいう事で、公匏で提䟛されおいるTodoアプリのコヌドを読んでみたした。

゜ヌス

公匏のサンプル。

いわゆるTodo管理ツヌルなんだけど、情報をHTML5のLocalStorageに持぀ようになっおいる。぀たりブラりザを閉じおも内容を蚘憶しおいる。

JSファむル

HTMLから読み蟌んでいるのは以䞋。

  • json2.js
  • jquery-1.7.1.js
  • underscore-1.3.1.js
  • backbone.js
  • backbone-localstorage.js
  • todos.js

Backbone.jsはUnderscore.jsずjQueryないしZepto.jsが必須。たたLocalStorageを䜿うためのBackbone拡匵を読み蟌んでいる。

json2.jsは䜕に䜿っおいるのかわからないけど、たぶんLocalStorageのI/OでJSONのパヌスをしおる、ずかかな。たああたり本線ずは関係ない。

で、todo.jsがこのアプリの本䜓。

党䜓を芋枡す

たずは詳现な実装を省略しお、倖偎党䜓を芋おみる。

  • todos.js
// An example Backbone application contributed by
// [Jrme Gravel-Niquet](http://jgn.me/). This demo uses a simple
// [LocalStorage adapter](backbone-localstorage.js)
// to persist Backbone models within your browser.

// Load the application once the DOM is ready, using `jQuery.ready`:
$(function(){

  // Todo Model
  // ----------

  // Our basic **Todo** model has `title`, `order`, and `done` attributes.
  var Todo = Backbone.Model.extend({
...
  });

  // Todo Collection
  // ---------------

  // The collection of todos is backed by *localStorage* instead of a remote
  // server.
  var TodoList = Backbone.Collection.extend({
...
  });

  // Create our global collection of **Todos**.
  var Todos = new TodoList;

  // Todo Item View
  // --------------

  // The DOM element for a todo item...
  var TodoView = Backbone.View.extend({
...
  });

  // The Application
  // ---------------

  // Our overall **AppView** is the top-level piece of UI.
  var AppView = Backbone.View.extend({
...
  });

  // Finally, we kick things off by creating the **App**.
  var App = new AppView;

});

ずいうわけで、こんなものを䜜っおいる事がわかる。

  • Todo … Todoのモデル。
    • 属性ずしお以䞋を持぀: title, order, done
  • TodoList … Todoのコレクション。耇数のモデルを栌玍し、たずめお扱う。
  • TodosTodoListのむンスタンス。コンストラクタじゃない。
  • TodoView … Todoのビュヌ。
  • AppView … アプリケヌション自䜓のビュヌ。
  • AppAppViewのむンスタンス。これもコンストラクタじゃない。

なんでむンスタンスを栌玍する倉数Todos, Appが倧文字で開始しおいるのかはよくわからない。あたり䞀般的な呜名芏則ではないず思う。

あずここを芋ただけでは、どこでナヌザヌむベントを拟っおいるのかずかがただわからない。たあ、慌おずに順に芋お行きたしょう。

Todoモデル

29行、メ゜ッドが四぀。これくらいなら読めそう。

  • default()
  • initialize()
  • toggle()
  • clear()

ここで実装しおるメ゜ッドは四぀だけど、さらにそこから別のメ゜ッドを呌んだりしおいる。モデルのメ゜ッドはBackbone.jsが提䟛するもので、それぞれドキュメントに掲茉されおた。ふむふむ。

default()

    // Default attributes for the todo item.
    defaults: function() {
      return {
        title: "empty todo...",
        order: Todos.nextOrder(),
        done: false
      };
    },

初期倀を返しおるっぜい。Todos.nextOrder()は埌ろの方70行目で実装しおる。特に問題なさそう。

initialize()

    // Ensure that each todo created has `title`.
    initialize: function() {
      if (!this.get("title")) {
        this.set({"title": this.defaults.title});
      }
    },

初期凊理。Todoのタむトルがなければ初期倀を蚭定、は良いのだけれど、this.defaults.titleが䞍思議なコヌド。this.defaults().titleではないのか 内郚的に䜕かやっおくれおるんだろうか。たぶんtypoだず思っおる。

get(), set()はモデルに栌玍されおいる情報を操䜜するアクセサみたい。情報はモデルのフィヌルドattributesから盎接操䜜する事も出来るみたいだけど、もちろんこれらのメ゜ッドを経由しお操䜜するのが「瀌儀正しい」んだろう。

toggle()

    // Toggle the `done` state of this todo item.
    toggle: function() {
      this.save({done: !this.get("done")});
    },

チェックをオン、オフする的な。倖郚から呌ばれそう。

save()を呌んで状態倉曎を保存しおいる。保存先の指定はないのかな。LosalStorageを䜿うラむブラリヌ (backbone-localstorage.js) を別途読み蟌んでいるんだけど、そっちの方で凊理を䞊曞きしたりしおくれおるんだろうか。

clear()

    // Remove this Todo from *localStorage* and delete its view.
    clear: function() {
      this.destroy();
    }

サペナラの術。これも倖郚から呌ばれるのかな。

destroy()はsave()しおある情報を削陀するもの。サヌバヌ今回はLocalStorageから情報を削陀。

Todoコレクション

31行、二぀のフィヌルドず四぀のメ゜ッド。

  • model
  • localStorage
  • done()
  • remaining()
  • nextOrder()
  • comparator()

内郚で呌んでるfilter(), without(), last() はUnderscore.jsの方のメ゜ッドらしい。(via Underscore Methods (28) )

model

    // Reference to this collection's model.
    model: Todo,

察象ずなるモデル。さっき䜜ったや぀を指定。

localStorage

    // Save all of the todo items under the `"todos"` namespace.
    localStorage: new Store("todos-backbone"),

Store は䞀緒に読み蟌んでる backbone-localstorage.js で定矩されおいた。

あ、そうか保存先はモデルじゃなくおコレクションの方で指定するのか。ぞえ。

そういうもの

done()

    // Filter down the list of all todo items that are finished.
    done: function() {
      return this.filter(function(todo){ return todo.get('done'); });
    },

チェックがオンのモデルだけ抜出しおくれるっぜい。

remaining()

    // Filter down the list to only todo items that are still not finished.
    remaining: function() {
      return this.without.apply(this, this.done());
    },

done()の逆版ぜい。なんか実行コスト高そう。

nextOrder()

    // We keep the Todos in sequential order, despite being saved by unordered
    // GUID in the database. This generates the next order number for new items.
    nextOrder: function() {
      if (!this.length) return 1;
      return this.last().get('order') + 1;
    },

新しく䜜るTodoの䜍眮を返す。䜍眮ずいうか、䜕ずいうか。

ちなみにモデルに䜍眮を蚘憶させるのは、ストレヌゞに保存した際は順序情報がないため、みたいなコメントが付いおる。

comparator()

    // Todos are sorted by their original insertion order.
    comparator: function(todo) {
      return todo.get('order');
    }

゜ヌトに甚いる倀を返すっぜい。䟋えば名前順にするなら、ここでtodo.get('title')おなもんにすりゃ良いんだろう。あずは日付ならUNIX時間に倉換しおから返すずか。いやDateオブゞェクトのたたでいいか。

Todoビュヌ

63行、結構長い。メ゜ッドが䞃぀、フィヌルドが䞉぀。

  • tagName
  • template
  • events
  • initialize()
  • render()
  • toggleDone()
  • edit()
  • close()
  • updateOnEnter()
  • clear()

tagName

    //... is a list tag.
    tagName:  "li",

はいはいタグの名前ですねヌっおのは良いんだけど、どこで䜿っおるのかわからない。内郚的に䜿っおいる

調べたら、やっぱり内郚的に䜿っおるみたい。埌述のrender()参照。

template

    // Cache the template function for a single item.
    template: _.template($('#item-template').html()),

Underscore.jsが提䟛するテンプレヌト。これはrender()で䜿っおる。

events

    // The DOM events specific to an item.
    events: {
      "click .toggle"   : "toggleDone",
      "dblclick .view"  : "edit",
      "click a.destroy" : "clear",
      "keypress .edit"  : "updateOnEnter",
      "blur .edit"      : "close"
    },

なんか"eventType selector": "methodName"っお感じでメ゜ッド呌んでくれそう。明瀺的に利甚しおいる箇所はない。うヌん、Backbone.Eventsの仕組みなのかなあ。

ず思ったら、あった。

> If an events hash is not passed directly, uses this.events as the source. Events are written in the format {"event selector": "callback"}. The callback may be either the name of a method on the view, or a direct function body.

意蚳する。

> 匕数でむベント情報が䞎えられない堎合はthis.eventsを利甚したす。むベント情報の曞匏は{"event selector": "callback"}です。コヌルバックはビュヌのメ゜ッド名か、盎接関数を指定する事もできたす。

自動的に呌ばれおるのかな。

initialize()

    // The TodoView listens for changes to its model, re-rendering. Since there's
    // a one-to-one correspondence between a **Todo** and a **TodoView** in this
    // app, we set a direct reference on the model for convenience.
    initialize: function() {
      this.model.bind('change', this.render, this);
      this.model.bind('destroy', this.remove, this);
    },

お、コメントが長めだ。意蚳しおみる。

> TodoViewはこのモデルの倉曎を監芖し、再描画したす。このアプリではTodoずTodoViewは䞀察䞀のやり取りなので、利䟿性のためにモデルを盎接参照しおいたす。

んヌ、あんたりよく分かんない。this.modelの事か。それずもメッセヌゞ送信の方かなあ。

model.bind()はドキュメントに掲茉されおいない。Underscore.jsのbind()だろうか。でも匕数が違うなあ。たあやっおる事は䜕ずなく想像が぀くけれど。

"change"ずか"destroy"ずかはむベントの皮類だろう。ドキュメントに䞀芧が掲茉されおいた。これだろな。

> Here’s a list of all of the built-in events that Backbone.js can fire. You’re also free to trigger your own events on Models and Views as you see fit. > > * "add" (model, collection) — when a model is added to a collection. > * "remove" (model, collection) — when a model is removed from a collection. > * "reset" (collection) — when the collection’s entire contents have been replaced. > * "change" (model, options) — when a model’s attributes have changed. > * "change:[attribute]" (model, value, options) — when a specific attribute has been updated. > * "destroy" (model, collection) — when a model is destroyed. > * "sync" (model, collection) — triggers whenever a model has been successfully synced to the server. > * "error" (model, collection) — when a model’s validation fails, or a save call fails on the server. > * "route:[name]" (router) — when one of a router’s routes has matched. > * "all" — this special event fires for any triggered event, passing the event name as the first argument.

ふむふむ。

぀たり結局のずころ、モデルに倉曎があればビュヌを再描画したりするわけだ。むベントを経由しおやり取りする事で疎結合になっお幞せ、ずいう話ですな。

render()

    // Re-render the titles of the todo item.
    render: function() {
      this.$el.html(this.template(this.model.toJSON()));
      this.$el.toggleClass('done', this.model.get('done'));
      this.input = this.$('.edit');
      return this;
    },

this.$elは自動で䜜られるみたい。どうやっお、ず思ったら、ここで先のtagNameが䜿われおるみたい。぀たりさっきliを指定しおいたから、ここでは既に<li />が䜜成され、this.$elに保存されおいる。詊しにTodoView.prototype.tagName='span'したら、<span />が䜜成されるようになった。正確に蚀うずノヌドのオブゞェクト自䜓ではなくお、それを持ったjQueryオブゞェクト。

で、その芁玠の内容を事前に甚意したtemplateを利甚しお、再構築ず。たたモデルからチェック状態を埗お反映させる。

$()は$el.find(selector)ず同じ。ショヌトカットずしお線集甚の芁玠このタむミングで䜜るのかをフィヌルドに保存しおいる。接頭蟞付けお$inputにした方が良いのではないかい。

return thisはよくわからないけど、他でも同じ事をやっおいるのを芋た。䜿い方ずしおは、view.render().$elみたいな感じで$elを曎新し぀぀利甚するみたい。曎新がなければ盎接view.$elずすればいいし。return this.$elにしないのは、先の二者間の統䞀感のためだろうか。

toggleDone()

    // Toggle the `"done"` state of the model.
    toggleDone: function() {
      this.model.toggle();
    },

おお、モデルで甚意したメ゜ッドを呌んでいる。ビュヌからモデルにメッセヌゞ送信メ゜ッド実行、なんかMVCぜい。

edit()

    // Switch this view into `"editing"` mode, displaying the input field.
    edit: function() {
      this.$el.addClass("editing");
      this.input.focus();
    },

線集モヌドに。

呌び出しはeventsで"dblclick .view" : "edit"ず蚭定しおいる。$el内でHTMLクラスviewを持぀芁玠がダブルクリックされるず、このメ゜ッドが実行される、ず。

なるほど。むベント曞くの、簡単でいいねえ。

close()

    // Close the `"editing"` mode, saving changes to the todo.
    close: function() {
      var value = this.input.val();
      if (!value) this.clear();
      this.model.save({title: value});
      this.$el.removeClass("editing");
    },

線集モヌドを終了しお、内容を確定し衚瀺を戻す。

updateOnEnter()

    // If you hit `enter`, we're through editing the item.
    updateOnEnter: function(e) {
      if (e.keyCode == 13) this.close();
    },

keypressむベントで実行されるもの。キヌコヌドを確認しお13 (Enter)なら線集終了。

clear()

    // Remove the item, destroy the model.
    clear: function() {
      this.model.clear();
    }

モデルぞ通知しお終わり。

モデルのclear()は前述の通り、destroy()するのみ。ただしビュヌのinitialize()でこんなコヌドがあった。

      this.model.bind('destroy', this.remove, this);

よっお、ビュヌのremove()が呌ばれる。

぀たりこういう颚に、玉突き匏に凊理が進むわけだ。

  1. view.clear()
  2. mode.clear()
  3. mode.destroy()
  4. view.remove()

ビュヌのremove()は$(view.el).remove();に同じ。

結局、DBからも画面からも消える。うヌん、疎結合はこういうずころが分かり蟛い。たあ仕方ないず思うけれど。

アプリビュヌ

これで最埌。86行にメ゜ッドが䞃぀ずフィヌルドが䞉぀。

いよいよ倧詰めです。

el

    // Instead of generating a new element, bind to the existing skeleton of
    // the App already present in the HTML.
    el: $("#todoapp"),

Backbone.jsのビュヌにはelず$elの二皮類がある。さっきTodoビュヌの方では自動生成される$elを䜿っおいたけれど、ここではel。

うヌん、よくわからない。たぶんelず$elの関係は$el == $(el)だず思うんだけど。

ずいうか、Todoで䜿っおいたのはthis.$elであっおextend()に䞎えるものじゃない。関係ないか。じゃあここのelは䜕なんだろう。セレクタヌ or オブゞェクト or 配列先頭を採甚おな感じかな。

アドバむスに埓いBackbone.jsの方を芋おみる。ちょろちょろ探したらこんなの出おきた:

たずはビュヌのコンストラクタ。_ensureElement()ずいうのを実行しおいる。

  • backbone.js #1141 (v0.9.2)
  // Creating a Backbone.View creates its initial element outside of the DOM,
  // if an existing element is not provided...
  var View = Backbone.View = function(options) {
    this.cid = _.uniqueId('view');
    this._configure(options || {});
    this._ensureElement();
    this.initialize.apply(this, arguments);
    this.delegateEvents();
  };

その䞭ではthis.elを確認しお、空ならthis.tagName等から芁玠を生成しお、setElement()しおいる。あるいはthis.elがそれが䜕であれtruthyなら、それでsetElement()。

  • backbone.js #1262 (v0.9.2)
    // Ensure that the View has a DOM element to render into.
    // If `this.el` is a string, pass it through `$()`, take the first
    // matching element, and re-assign it to `el`. Otherwise, create
    // an element from the `id`, `className` and `tagName` properties.
    _ensureElement: function() {
      if (!this.el) {
        var attrs = getValue(this, 'attributes') || {};
        if (this.id) attrs.id = this.id;
        if (this.className) attrs['class'] = this.className;
        this.setElement(this.make(this.tagName, attrs), false);
      } else {
        this.setElement(this.el, false);
      }
    }

枡されたelementを確認。 $jQueryないしZeptoのむンスタンスなら、それをthis.$elずしお蚘憶。そうでないなら、$()を通しおから同様にする。$()に䞎えるのはセレクタヌでもDOMノヌドのオブゞェクトでも良いので、皆が幞せになれるずいうわけだ。

  • backbone.js #1201 (v0.9.2)
    // Change the view's element (`this.el` property), including event
    // re-delegation.
    setElement: function(element, delegate) {
      if (this.$el) this.undelegateEvents();
      this.$el = (element instanceof $) ? element : $(element);
      this.el = this.$el[0];
      if (delegate !== false) this.delegateEvents();
      return this;
    },

なるほど。ずいうわけで解決した。わヌい。

statsTemplate

    // Our template for the line of statistics at the bottom of the app.
    statsTemplate: _.template($('#stats-template').html()),

Todoビュヌず同じく_.template()。

あちらはフィヌルド名がtemplateだったが、こちらはstatsTemplateになっおいるのは、䜕か意味があるんだろうか。

events

    // Delegated events for creating new items, and clearing completed ones.
    events: {
      "keypress #new-todo":  "createOnEnter",
      "click #clear-completed": "clearCompleted",
      "click #toggle-all": "toggleAllComplete"
    },

これもTodoビュヌず同じ。

initialize()

    // At initialization we bind to the relevant events on the `Todos`
    // collection, when items are added or changed. Kick things off by
    // loading any preexisting todos that might be saved in *localStorage*.
    initialize: function() {

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

      Todos.bind('add', this.addOne, this);
      Todos.bind('reset', this.addAll, this);
      Todos.bind('all', this.render, this);

      this.footer = this.$('footer');
      this.main = $('#main');

      Todos.fetch();
    },

これはちょいず長め。ずいっおも倧した事はなにもしおいない。

たず冒頭のコメントを意蚳する。

> 初期凊理でTodoコレクションTodosに、項目が远加されたり倉曎されたりしたずきのむベントを登録したす。 > LocalStorageに保存されおいる既存のTodo項目の読み蟌んで、あれこれを開始したす。

これで䜜成するオブゞェクトを「アプリケヌションの本䜓」ず呌んで良いのだろう。

this.allCheckboxがjQueryオブゞェクトではなく生のDOMノヌドにしおいるのは䜕か意味があるのだろうか。その方が早いっおのはあるだろうけれど。

render()

    // Re-rendering the App just means refreshing the statistics -- the rest
    // of the app doesn't change.
    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();
      }

      this.allCheckbox.checked = !remaining;
    },

これもたた長め。Todoビュヌのrender()ず同じく、芁玠の再描画を行う凊理。

Todoに登録があれば䞀芧ずフッタヌを衚瀺し、そうでなければ非衚瀺に。たた党おチェック枈みなら「党チェック」のチェックボックスもオンにしたす。

addOne()

    // Add a single todo item to the list by creating a view for it, and
    // appending its element to the `<ul>`.
    addOne: function(todo) {
      var view = new TodoView({model: todo});
      this.$("#todo-list").append(view.render().el);
    },

Todoリストのaddに実行されるよう、initialize()のずこで指定されおた凊理。

Todoのビュヌを生成しお、その芁玠をアプリ内の䞀芧に远加。芁玠の远加はいいけど、Todoビュヌの生成はここでやる事なのか Todoリストの方でそこたでやっおおいお欲しいような。いや、あちらはあくたでモデルのコレクションだから、ビュヌには觊れないべきか。するず、やはりここでTodobビュヌを生成するのがよろしいか。

addAll()

    // Add all items in the **Todos** collection at once.
    addAll: function() {
      Todos.each(this.addOne);
    },

Todoリストのresetに実行されるよう、initialize()のずこで指定されおた凊理。

各モデルごずにビュヌを䜜成したりする。

createOnEnter()

    // If you hit return in the main input field, create new **Todo** model,
    // persisting it to *localStorage*.
    createOnEnter: function(e) {
      if (e.keyCode != 13) return;
      if (!this.input.val()) return;

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

新芏䜜成の入力欄でキヌ抌䞋時に実行されるよう、eventsで指定されおた凊理。

入力があるずきにEnterキヌが抌されるず、Todoをひず぀䜜成する。モデル䜜成ず同時にコレクションに远加するので、new TodoではなくおTodos.create()なんだろう。

むベント経由でTodoビュヌも䜜成される。

clearCompleted()

    // Clear all done todo items, destroying their models.
    clearCompleted: function() {
      _.each(Todos.done(), function(todo){ todo.clear(); });
      return false;
    },

チェック枈みのTodoのモデルのclear()を呌ぶ。内郚ではBackbone.Modelのdestroy()が呌ばれ、そこからむベント経由でビュヌのremoveが実行される。

toggleAllComplete()

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

「党チェック」のオン・オフ時に実行されるよう、eventsで指定されおた凊理。

おしたい

党䜓で250行足らずのコヌド。綺麗に分割されおいる事がよく分かった。DOMやむベントたわりの抜象化は結構奜みだ。

䜿い方の方もなんずなく芋えおきたけれど、LocalStorage以倖を䜿うのにどうするのかずか、デフォルトだずたぶんサヌバヌず通信するんだけど、そこんずこどうなのっおのがただわからない。たあ、おいおい芋お行きたしょう。