カテゴリ: jQuery

backbone.jsの公式ページを上から下まで眺めてみる

夜です。今回は何を思ったのか、backbone.jsの公式ページを眺めるという行為に及んでみたいと思います。 ちょっと見てみると、backbone.jsのルーティングがなんか便利そうじゃないですか。pushState触らなくていいとか! ajax使うにはもうbackboneは手放せない存在なんじゃないでしょうか。githubは違うプラグイン使っているみたいですが… 公式ページ。backbone.jsの配布ページとマニュアルページがひとつに繋がっているタイプ。Ctrl+Fで検索しやすいのでなかなかいいかも。

Introduction

Google翻訳を使ってIntroductionを読んでみます。要約すると(確実に翻訳ミスがあります)
  • jQueryをDOMを直接操作してHTMLを動的に動かす開発をしてるんでしょうけど、古いよ
  • フロントエンドとサーバー間でデータの受け渡しに四苦八苦してる
  • Backbone.jsを使うとデータの受け渡し楽よ
  • Backbone.jsを使うとデータに変化が合った場合changeイベントが走って画面が動的に書き換わるから楽よ
  • Backbone.jsを使うと手作業でHTML更新しなくてすむよ
  • Backbone.jsがどういうものなのか下にたくさんサンプルスクリプト用意したから見てね
  • 再生ボタン押すとサンプルスクリプトを直接実行できるよ
という感じな気がする。多分。

Upgrading to 1.0

1.0の変更点などが書いてある。あまり関係なさそうなので省略。

Backbone.Events

イベントは、任意のオブジェクトにイベントドリブンなメソッドを提供したりできるようです。
var object = {};
_.extend(object, Backbone.Events);Code language: JavaScript (javascript)
とするだけで、変数objectは object.on と object.trigger というメソッドを持つようになりました。onはイベントの登録、triggerはイベントの実行です。
var object = {};

// 変数objectで object.on と object.trigger を使えるようにした
_.extend(object, Backbone.Events);

// alertという名前でイベントを登録
object.on("alert", function(msg) {
  alert("Triggered " + msg);
});

// イベントの実行
object.trigger("alert", "an event");Code language: JavaScript (javascript)
なるほど便利、 ちなみに _.extend は underscore.jsの機能で、オブジェクトに異なるオブジェクトのメソッドをもたせることができる、いわゆるサブクラスっぽいことができるメソッド。triggerで引数をわたすこともできるようです。 また、
var dispatcher = _.clone(Backbone.Events);Code language: PHP (php)
でディスパッチャの切り分けができます。

object.on(event, callback, [context])Alias: bind

オブジェクトに登録したイベントが発生するたびにコールバック関数が呼び出されます。 オブジェクトに登録したイベントが多すぎるときに分類するときは”hoge:[attribute]”と書くと良いようです。 スペース区切りすると、複数のイベントを登録させることができます。
book.on("change:title change:author", ...);Code language: CSS (css)
第三パラメータに値またはオブジェクトを渡すと、それがコールバック関数内で、thisになります。
model.on('change', this.render, this);

model.on('change', this.render, {hoge: 1});
Code language: JavaScript (javascript)
all という名前のイベントを登録すると、オブジェクトから様々なイベントが実行された後に一緒にallイベントのコールバック関数も実行されるようになります。
object.on("all", function(eventName) {
  // eventNameには実行されたイベントの名前が入っています
  console.log(eventName);
});
Code language: JavaScript (javascript)
このような書き方(イベントマップ)でもイベントを登録することができます。これだと一括でイベントが登録できるので楽。
book.on({
  "change:title": titleView.update,
  "change:author": authorPane.update,
  "destroy": bookView.remove
});Code language: JavaScript (javascript)

object.off([event], [callback], [context]) Alias: unbind

オブジェクトから以前登録されたイベントのコールバック関数を削除するメソッドです。 コンテキストが指定されていない場合は、コールバックのすべてのイベントが削除される。 コールバックが指定されていない場合は、イベントのすべてのコールバックが削除。 イベントが指定されていない場合は、すべてのイベントのコールバックは削除。 event→callback→contextと削除される条件と範囲が変わるようです。ということは、同じイベント名でもコールバック関数が違えば、上書きされず2つとも実行される仕様のようですね。下記スクリプトでテストしてみることにします。
var o = {};
_.extend(o, Backbone.Events);

o.on("hoge", function() {
  console.log("hoge1");
});
o.on("hoge", function() {
  console.log("hoge2");
});

o.trigger("hoge");

#=> hoge1
#=> hoge2
Code language: PHP (php)
2つ出力されたので、2つのコールバック関数が実行されることが確認出ました。イベント名が同じ場合、オーバーライド(上書き)されるのではなく、順々に実行されるようです。 offメソッドの使い方は下記の通り。
// changeイベントに登録されているonChangeコールバック関数の登録解除
object.off("change", onChange);

// change イベント に登録されている全てのコールバック関数の登録解除
object.off("change");

// 全てのイベントのonChangeコールバック関数を登録解除
object.off(null, onChange);

// 全てのイベントの全てのコールバックのcontextを持っている全てのコールバック関数の登録解除
object.off(null, null, context);

// objectに登録されている全てのコールバック関数の登録解除
object.off();Code language: JavaScript (javascript)
onメソッドの使い方サンプルではよくわかりませんでしたが、offメソッドのサンプルを見ることで、backboneのイベントがどこまで柔軟なことができるか目に見えてわかりますね。。null = 全て の範囲はおそらく、backbone.Eventsを使って登録したイベントの範囲なのでしょうから、
var dispatcher = _.clone(Backbone.Events);Code language: PHP (php)
によってディスパッチャをわけることで、この全ての範囲をわけることも可能だということにメリットも感じられます。

object.trigger(event, [*args])

eventを実行します。”event event”の書き方であればそれぞれのイベントを実行します。 argsでは,区切りで引数を付けることができます。 ただそれだけ!とてもシンプル!!

object.once(event, callback, [context])

一度実行されたら削除されるイベントです。
var o = {};
_.extend(o, Backbone.Events);

o.on("hoge", function() {
  console.log("hoge1");
});
o.once("hoge", function() {
  console.log("hoge2");
});

o.trigger("hoge");
o.trigger("hoge");Code language: JavaScript (javascript)
イベントといってもコールバック関数が削除されるので、hoge1コールバック関数は消されずに残っています。hoge2だけが実行されません。なるほど便利。

object.listenTo(other, event, callback)

多分、後でlistenToについて説明した項があるんでしょうけど、やっぱり気になるのでここで説明すると、otherは var model = new Backbone.Model(); で作ったモデルに対してeventが発生すると、callbackを実行してくれるというもの。
var o = {};
_.extend(o, Backbone.Events);

var model = new Backbone.Model();

o.listenTo(model, 'change', function () {
    console.log("モデルデータに変化がありました", this);
});

model.set("hoge", "ほげほげ");

// => モデルデータに変化がありました Code language: JavaScript (javascript)
サンプルだと、モデルに変化があると実行してくれます。 this の中身は o になっているので、モデルに対して o のメソッドが使えるので、 o のメソッドを使ってモデルに対していろんなことができます。なるほど、便利。 眠いけど、どんどんいきます。

object.stopListening([other], [event], [callback])

listenToの逆、イベントの監視をストップします。
// viewの監視を全てストップ
view.stopListening();

// viewのmodelに対する監視をストップ
view.stopListening(model);Code language: JavaScript (javascript)

object.listenToOnce(other, event, callback)

ちょうどlistenToを同じような振る舞いをしますが、一度監視でコールバック関数が実行されると、監視を終了します。
var o = {};
_.extend(o, Backbone.Events);

var model = new Backbone.Model();

o.listenToOnce(model, 'change', function () {
    console.log("モデルデータに変化がありました", this);
});

model.set("hoge", "ほげほげ");
// => モデルデータに変化がありました 

model.set("hoge", "ほげほげ");
// => Code language: JavaScript (javascript)
ずいぶんとやる気のないメソッドですね。

Catalog of Events

モデルやビューやコレクションに対して独自にイベントを定義し利用することは自由だけれど、そのほかにBackbone.jsが独自に追加しているイベントもあるようで、それが下記の通り。
add
モデルがコレクションに追加されたとき呼ばれます。

remove
モデルがコレクションから削除されたとき呼ばれます。

reset
コレクションが新しく作りなおされたときに呼び出されます。(あいまい)

sort
コレクションが再ソートされたとき呼ばれます。

change
モデルの属性が変更されたとき呼ばれます。(よく使われます)

change:[attribute]
モデルの持っている特定の値が変更されたときのみ呼ばれます

destroy
モデルが削除されたとき呼ばれます。


request
モデルまたはコレクションがサーバーにリクエストを送った時に呼ばれます。


sync
モデルまたはコレクションが正常にサーバーと同期されているときに呼ばれます。


error
モデルの内容をサーバーに送り保存しようとしたときに、サーバーエラーが発生した場合呼ばれます。


invalid
モデルの検証がクライアント上で失敗したときに呼ばれます。


route:[name]
指定されたルーティングが一致しているとき実行します


route
任意のルーティングが一致したとき、または履歴によって一致したときに呼び出されます。


all
このイベントは、第一パラメータにイベント名を渡し、あらゆるイベントに対しても呼び出されます。Code language: CSS (css)
わかるのもありけど、よくわからないのもありますね。ルーティング…? また、これらイベントは model.set(), collection.add 等のメソッドによって呼ばれるので、 model[“hoge”] = 1 なんてしても呼ばれることはないようです。また、model.set等でモデルにデータを追加した場合に、イベントが呼ばれるのが嫌な場合は、
var o = {silent: true};
_.extend(o, Backbone.Events);

var model = new Backbone.Model({silent: true});

o.listenToOnce(model, 'change', function () {
    console.log("モデルデータに変化がありました", this);
});

model.set("hoge", "ほげほげ", {silent: true});
// => 
Code language: JavaScript (javascript)
というよう第三パラメータ、 silent: true を渡すことで、イベントが発生しないようです。

Backbone.Model

ふぅ… いよいよModelへはいります。 Modelというのはデータ構造を表します。というか、データそのものです。身近なものだと、アドレス帳とか、日記とかソシャゲのガチャで集めたカードなどといったデータをまとめてモデルと言います。 Backbone.jsにおけるモデルは、JavaScriptの心臓部です。Backbone.jsはモデルに対して変換や、検証や、プロパティの追加削除機能等 様々な機能を提供します。 以下のサンプルスクリプトは公式にあげられていたものをそのままコピーしたものに日本語コメントを付与したものです。サイドバーの背景色を変更するだけの簡単なスクリプトになっています。公式ページでは再生ボタンを押すことでスクリプトを実行することができます。一度実行すると、ChromeであればWeb Developper Toolsによってモデルの書き換えを行い自由に色を変更することができるので、モデルを変更することでコールバックを実行するといった一連のフローを理解することができます。
// Backboneモデルクラスを作成し、そのモデルクラスに色を変更するメソッドを付けて拡張します
var Sidebar = Backbone.Model.extend({
  promptColor: function() {
    var cssColor = prompt("Please enter a CSS color:");
    this.set({color: cssColor});
  }
});

// Sidebarモデルクラスから window.sidebar モデルを作成します
window.sidebar = new Sidebar;

// change:color イベントが発生したら、コールバック関数によってjQueryを使用し背景色を変更するようにします
sidebar.on('change:color', function(model, color) {
  $('#sidebar').css({background: color});
});

// サイドバーの色を白に変更します
sidebar.set({color: 'white'});

// サイドバーの色を何色にするかクライアントに問いかけます
sidebar.promptColor();Code language: JavaScript (javascript)
ソースコードの動きが読めたでしょうか、通常のコーディングでは、ボタンを押したらここの背景を変えて…みたいな処理を書いていたとおもいますが、このソースコードでは、sidebarモデルのcolorプロパティの値をwhiteに変えただけで、サイドバーの背景色が変わっています。データだけ見ることによって、ロジックまで来にしなくて済むのです。なるほど、便利。

Backbone.Model.extend(properties, [classProperties])

extendによって、Backbone.Modelを拡張し、独自のモデルクラスを生成することができます。 このモデルクラスを利用してモデルを作ると、そのモデルはpropertiesのプロパティを持つようになります。
// Nodeモデルクラスを生成
var Note = Backbone.Model.extend({
  initialize: function() {},
  author: function() {},
  coordinates: function() {},
  allowedToEdit: function(account) {
    return true;
  }
});


// PrivateNoteモデルクラスを生成
var PrivateNote = Note.extend({
  allowedToEdit: function(account) {
    return account.owns(this);
  }
});Code language: JavaScript (javascript)
また、定義済みのメソッドをオーバーライドしたい場合、簡単にオーバーライドできるようにな仕組みは存在ず、めんどくさいけども下記のようなやりかたで実現可能です。
var Hoge = Backbone.Model.extend({
  set: function(attributes, options) {
    Backbone.Model.prototype.set.apply(this, arguments);
    console.log("値をセットしました");
  }
});
var a = new Hoge();
a.set("hoge", "1");
console.log(a.get("hoge"));Code language: JavaScript (javascript)

constructor / initializenew Model([attributes], [options])

モデルを生成するときに、デフォルトの値を最初から定義したり、コンストラクタ(モデルを生成するタイミングで呼ばれるメソッド)を定義したりすることができます。 モデルを作成するタイミングでデフォルトの値を最初から定義する場合は、
// モデルに値を入れて生成
new Book({
  title: "One Thousand and One Nights",
  author: "Scheherazade"
});Code language: JavaScript (javascript)
とします。 モデルを生成するタイミングでメソッドを呼び出したい場合は
var Library = Backbone.Model.extend({
  constructor: function() {
    this.books = new Books();
    Backbone.Model.apply(this, arguments);
  },
  parse: function(data, options) {
    this.books.reset(data.books);
    return data.library;
  }
});Code language: JavaScript (javascript)
このようにします。

model.get(attribute)

モデルから指定されたattributeに定義されてある値を取得します。

model.set(attributes, [options])

モデルに値をセットします。モデルに値をセットすると、changeイベントが発生します。 change:[attribute] というイベントが登録されていた場合は、その[attribute]が変更された場合のみそのイベントに関連付けられたコールバック関数が実行します。
var a = new Backbone.Model();
a.on("change", function () {
    console.log("変更されましたA");
});
a.on("change:hoge", function () {
    console.log("変更されましたB");
});
a.on("change:hoge2", function () {
    console.log("変更されましたC");
});
a.set("hoge", "1");
// => 変更されましたB
// => 変更されましたA
Code language: JavaScript (javascript)
モデルはオブジェクトのように定義することもできます。
note.set({title: "March 20", content: "In his eyes she eclipses..."});Code language: CSS (css)

model.escape(attribute)

getメソッドと似通ってはいるけど、これはHTMLをエスケープしたものを返すので、XSS対策を行うことができます。(エスケープは、HTMLエンティティ化です)
var a = new Backbone.Model({
    hoge: "<script>alert(1);</script>"
});
a.escape("hoge");
// => &lt;script&gt;alert(1);&lt;&#x2F;script&gt;Code language: HTML, XML (xml)

model.has(attribute)

modelがattributeを持っている場合trueを返します
var a = new Backbone.Model({
    hoge: "hoge"
});
console.log(a.has("hoge"));
// => true
console.log(a.has("hogehoge"));
// => false
Code language: JavaScript (javascript)

model.unset(attribute, [options])

modelのattributeで指定されたプロパティを削除します。optionにsilent: true が指定されな限りchangeイベントが呼ばれます。
var a = new Backbone.Model({
    hoge: "hoge"
});
a.unset("hoge");
console.log(a.has("hoge"));
// => false
Code language: JavaScript (javascript)

model.clear([options])

modelからid属性を含む全てのプロパティを削除します。optionsにsilent:true が指定されていない限り、changeイベントが発生します。
var a = new Backbone.Model({
    hoge: "hoge"
});
a.clear();
console.log(a.has("hoge"));
// => false
Code language: JavaScript (javascript)

model.id

model.idにはユニークな値を指定することができます。特に使い方が今の段階では不明。。

model.idAttribute

model.idとして取得できる別な属性を指定することができます。これは、データベースのテーブルからデータを引っ張ってきたときにカラム名がidではない違う名前だった場合便利です。
var Meal = Backbone.Model.extend({
  idAttribute: "_id"
});

var cake = new Meal({ _id: 1, name: "Cake" });
alert("Cake id: " + cake.id);Code language: JavaScript (javascript)
なるほど、データベースとのデータの整合性マッチとして model.id は使える。便利。

model.cid

自動的にすべてのモデルに割り当てられた一意の識別子です。 cidは、モデルがまだサーバーに保存されていない場合に便利です。(cidがそのままサーバーに登録するレコードidとして使える)

model.attributes

modelの内包しているオブジェクトがそのまま返ります。
var a = new Backbone.Model({
    hoge1: "hoge1",
    hoge2: "hoge2"
});
console.log(a.attributes)
// => {hoge1: "hoge1", hoge2: "hoge2"}
Code language: JavaScript (javascript)
attributesによってモデルデータを直接書き換えたりすることもできます。モデルデータをコピーしたい場合は
_.clone(model.attributes);Code language: CSS (css)
を使用します。 ただ、モデルデータを直接書き換える場合、プロパティ名にキーを含ませることが可能になってしまうので、それをやってしまうと、”イベントをスペースで区切る”

model.changed

最後に変更された属性がオブジェクト(ハッシュ)で返ります。変更差分のみを保存させるロジックを書くのに便利です。
var a = new Backbone.Model({
    hoge1: "hoge1",
    hoge2: "hoge2"
});
a.set("hoge1", "new");
console.log(a.changed);
// => Object {hoge1: "new"}
Code language: JavaScript (javascript)

model.defaults or model.defaults()

デフォルト値の取得および設定ができます。
var Meal = Backbone.Model.extend({
  defaults: {
    "appetizer":  "caesar salad",
    "entree":     "ravioli",
    "dessert":    "cheesecake"
  }
});
Meal.prototype.defaults["hoge"] = "hoge";
var a = new Meal();
console.log(a.defaults);
// => Object {appetizer: "caesar salad", entree: "ravioli", dessert: "cheesecake", hoge: "hoge"}Code language: JavaScript (javascript)
デフォルト値は、モデルクラスの参照によって渡されるので、書き換えには注意が必要です。

model.toJSON()

モデルをJSON文字列に変換するためのオブジェクトを返します。このメソッドは、一見JSON文字列を返してくれるように見えますが、JSON.stringifyによって変換するためのオブジェクトが用意されるだけのものです。また、JSON.stringifyを使用する場合、toJSONが暗黙的に呼び出されるようです。(省略できます)
var a = new Backbone.Model({
    hoge1: "hoge1",
    hoge2: "hoge2"
});
console.log(a.toJSON());

console.log(JSON.stringify(a));

// => Object {hoge1: "hoge1", hoge2: "hoge2"}
// => {"hoge1":"hoge1","hoge2":"hoge2"}Code language: JavaScript (javascript)

model.sync(method, collection, [options])

サーバーへモデルの状態を保持する Backbone.syncを使用するためのオプションです。

model.fetch([options])

Backbone.syncを利用することで、モデルの状態を再設定します。 現在のモデルデータをサーバーから取得したモデルデータと比較して、差異が合った場合やローカル側のモデルデータが空になっている場合はchangeイベントが発生します。
// ajax通信方式
var BookClass = Backbone.Model.extend({
  url: "/ajax/hogehoge"
}); 
var book = new BookClass({
  title: "The Rough Riders",
  author: "Theodore Roosevelt"
});
book.fetch();


// Backbone.sync方式
Backbone.sync = function(method, model) {
  console.log(method + ": " + JSON.stringify(model));
};
var BookClass = Backbone.Model.extend({
  url: "/ajax/hogehoge"
}); 
var book = new BookClass({
  title: "The Rough Riders",
  author: "Theodore Roosevelt"
});
book.fetch();Code language: JavaScript (javascript)

model.save([attributes], [options])

Backbone.syncによってモデルデータを保存します。検証に成功した場合、jqueryXHRを返します。 変更したいプロパティのみを記述し、記述されないプロパティはサーバー上から変更されません。 save時には、modelのvalidateメソッドによってデータの検証が行われるので、検証に失敗した場合は保存されません。 モデルがすでにサーバー上にある場合、HTTPメソッドはPUTとなり、ない場合は、POSTとなります。 saveが実行されると、 モデルイベントが、 change、request、syncの順番で発生します。 基本的に、saveは非同期で行われますが、サーバーの処理を待ちたい場合は、optionsに wait: true を指定します。
// 同期処理
Backbone.sync = function(method, model) {
  console.log(method + ": " + JSON.stringify(model));
  model.id = 1;
};

// モデル作成
var book = new Backbone.Model({
  title: "The Rough Riders",
  author: "Theodore Roosevelt"
});

// モデルをサーバーに保存
book.save();

// プロパティを書き換えて保存
book.save({author: "Teddy"});

console.log(book.attributes);

// => create: {"title":"The Rough Riders","author":"Theodore Roosevelt"}
// => update: {"title":"The Rough Riders","author":"Teddy"}
// => Object {title: "The Rough Riders", author: "Teddy"}Code language: JavaScript (javascript)
検証に失敗した場合や、通信時にエラーがあった場合は error を指定するとエラー通知を行うことができます。
book.save("author", "F.D.R.", {error: function(){ ... }});Code language: CSS (css)

model.destroy([options])

Backbone.syncによって、HTTPメソッドのDELETEによってモデルをサーバー上のデータベーステーブルから削除してもらうよう命令することができます。
// 同期処理
Backbone.sync = function(method, model) {
  console.log(method + ": " + JSON.stringify(model));
};

// モデル作成
var book = new Backbone.Model({
  id: 1,
  title: "The Rough Riders",
  author: "Theodore Roosevelt"
});

// 削除
book.destroy({success: function(model, response) {
  console.log("--");
}});
// => delete: {"id":1,"title":"The Rough Riders","author":"Theodore Roosevelt"}Code language: JavaScript (javascript)
モデルにid属性がない場合は、destroyは失敗しfalseが返ります。

Underscore Methods (6)

Underscoreの基本的なメソッドをモデルに対して使用することができます。_を使わなくても、モデルのメソッドとしてデリゲート(委任)されているのでそのままモデルに対して使用できます。
var a = new Backbone.Model({
    hoge1: "hoge1",
    hoge2: "hoge2",
    hoge3: "hoge3"
});

// 許可されたキーだけの抽出したハッシュを生成
console.log(a.pick("hoge1", "hoge2"));
// => Object {hoge1: "hoge1", hoge2: "hoge2"}

// 指定したキーを除外したハッシュを生成
console.log(a.omit("hoge1"));
// => Object {hoge2: "hoge2", hoge3: "hoge3"}

// モデルの全てのキー名を取得
console.log(a.keys());
// => ["hoge1", "hoge2", "hoge3"]

// モデルの全ての値を取得
console.log(a.values());
// => ["hoge1", "hoge2", "hoge3"]

// オブジェクトを [key, value] の形式に変換したものを生成します
console.log(a.pairs());
// => [["hoge1","hoge1"],["hoge2","hoge2"],["hoge3","hoge3"]]

// キーとバリューを入れ替えたものを生成します
console.log(a.invert());
// => Object {hoge1: "hoge1", hoge2: "hoge2", hoge3: "hoge3"}Code language: JavaScript (javascript)

model.validate(attributes, options)

モデルの検証を定義するためのメソッドです。検証ロジックはデフォルトでは未定義(特に何もしない)担っています。 モデルデータによってJavaScriptの任意のコードが実行できるようなものやバックエンドに何らかの影響を及ぼすようなものに対してvalidateの定義が推奨されています。 検証は、saveメソッドを使用する際に呼び出されますが、 model の options に {validate:true} を指定することで、saveする前に呼び出すことができます。また、検証に失敗するとデータベースに変更は保存しません。
var Chapter = Backbone.Model.extend({
  validate: function(attrs, options) {
    if (attrs.end < attrs.start) {
      return "開始時間より終了時間のほうが早くなっているため保存出来ません";
    }
  }
});

var one = new Chapter({
  title : "test"
});

one.on("invalid", function(model, error) {
  console.error(model.get("title") + ": " + error);
});

one.set("start", 15);
one.set("end", 10,  {validate:true});
// => 開始時間より終了時間のほうが早くなっているため保存出来ません

console.log(one.get("end"));
// => undefined

one.save({
  start: 15,
  end:   10
});
// => 開始時間より終了時間のほうが早くなっているため保存出来ません
Code language: JavaScript (javascript)
検証に失敗したモデルに対して invalid イベントが発生されます。

model.validationError

検証に最後に失敗した際のエラー情報が返ります。
one.validationError
// => "開始時間より終了時間のほうが早くなっているため保存出来ません"Code language: JavaScript (javascript)

model.isValid

検証を直接実行します。
var Chapter = Backbone.Model.extend({
  validate: function(attrs, options) {
    if (attrs.end < attrs.start) {
      return "開始時間より終了時間のほうが早くなっています";
    }
  }
});

var one = new Chapter({
  title : "test"
});

one.set({
  start: 15,
  end:   10
});

if (!one.isValid()) {
  console.error(one.get("title") + ": " + one.validationError);
}Code language: JavaScript (javascript)

model.url()

モデルに紐付けられるURLを返します。(サーバー上のリソース) urlRoot属性がモデルクラスにない場合このメソッドは失敗します。

model.urlRoot or model.urlRoot()

モデルをHTTP経由のリソースの場合対応するURLが必要になってきます。その際に便利なのが。urlRootです。urlRootを定義することでモデルからURLを取得することができるようになります。また、このurlRootはコレクションに対しても有効です。
var Book = Backbone.Model.extend({urlRoot : '/books'});
var solaris = new Book({id: "1083-lem-solaris"});
console.log(solaris.url());Code language: JavaScript (javascript)

model.parse(response, options)

parseはデフォルトではresponseをそのまま戻り値として返すだけのメソッドです。extendで処理をオーバーライドして使用します。fetchやsaveを実行する際に、取得する前のデータをちょっと加工したりするのに使います。
// 同期処理
Backbone.sync = function(method, model) {
  console.log(method + ": " + JSON.stringify(model));

  // サーバーからデータが送られてきたと仮定
  if (method == "read") {

    // model.parseしてからmodelにセット
    model.set(model.parse({
        data: {
            "key1": 1,
            "key2": 2,
            "key3": 3
        }
    }));
  }
};

// モデルクラス作成
var modelClass = Backbone.Model.extend({
  parse: function (obj) {
    return obj["data"];
  }
});

// モデル作成
var book = new modelClass({
    id: 1,
    title: "The Rough Riders",
    author: "Theodore Roosevelt"
});

// サーバーからデータを受信
book.fetch();

// テスト
console.log(book.get("key1"));
// => 1Code language: JavaScript (javascript)
また、モデル作成時のデフォルトのデータ指定時に、オプションに { parse: true } を指定すると、内部でparseが呼び出されデータを加工してからモデルにデータが追加されます。
// モデルクラス作成
var modelClass = Backbone.Model.extend({
  parse: function (obj) {
    return obj["data"];
  }
});

// モデル作成
var book = new modelClass({
  data: {
    id: 1,
    title: "The Rough Riders",
    author: "Theodore Roosevelt"
  }
},
{
  parse: true
});

// テスト
console.log(book.get("title"));
// => The Rough RidersCode language: JavaScript (javascript)

model.clone()

モデルのディープコピーを行います。モデルは完全に別々のものとして扱われます。
var a = new Backbone.Model({
    hoge1: "hoge1",
    hoge2: "hoge2",
    hoge3: "hoge3"
});



var b = a.clone();

console.log(b.attributes);
// => Object {hoge1: "hoge1", hoge2: "hoge2", hoge3: "hoge3"}Code language: JavaScript (javascript)

model.isNew()

サーバー上に保存されておらず、新しく作られたモデルである場合 true を返します。
var a = new Backbone.Model({
    hoge1: "hoge1",
    hoge2: "hoge2",
    hoge3: "hoge3"
});


console.log(a.isNew());
// => trueCode language: JavaScript (javascript)

model.hasChanged([attribute])

モデルのプロパティである [attribute] に変更があった場合、 true を返します。
var a = new Backbone.Model({
    hoge1: "hoge1",
    hoge2: "hoge2",
    hoge3: "hoge3"
});

a.on("change", function () {

  if (a.hasChanged("hoge1")) {
    console.log("hoge1に変更がありました");
  }

  if (a.hasChanged("hoge2")) {
    console.log("hoge2に変更がありました");
  }

  if (a.hasChanged("hoge3")) {
    console.log("hoge3に変更がありました");
  }

});

a.set("hoge1", "1");
// => hoge1に変更がありましたCode language: JavaScript (javascript)

model.changedAttributes([attributes])

modelの変更点を返します。[attributes]にハッシュを指定すると、そのハッシュが返ります。
var a = new Backbone.Model({
    hoge1: "hoge1",
    hoge2: "hoge2",
    hoge3: "hoge3"
});

a.set("hoge3", "1");

console.log(a.changedAttributes());
// => Object {hoge3: "1"}
Code language: JavaScript (javascript)

model.previous(attribute)

プロパティの値が変更される前の値を取得します。
var bill = new Backbone.Model({
  name: "スーパーサイヤ人"
});

bill.on("change:name", function(model, name) {
  console.log(bill.previous("name"));
  console.log("↓");
  console.log(name);
});

bill.set({name : "スーパーサイヤ人2"});
// => スーパーサイヤ人
// => ↓
// => スーパーサイヤ人2Code language: JavaScript (javascript)

model.previousAttributes()

モデルの変更前の全ての属性を返します。 前の状態に戻るとき(ロールバック)に便利です。
var bill = new Backbone.Model({
  name: "スーパーサイヤ人"
});

bill.on("change", function(model) {
  console.log(bill.previousAttributes());
  console.log("↓");
  console.log(bill.attributes);
});

bill.set({name : "スーパーサイヤ人2"});
// => Object {name: "スーパーサイヤ人"}
// => ↓
// => Object {name: "スーパーサイヤ人2"}Code language: JavaScript (javascript)

Backbone.Collection

やっとモデルが終わりました。覚えること多くて大変です。次に、コレクションを眺めていきます。 コレクションは、名前の通りコレクションで、何をコレクションするかというとモデル。複数のモデルを一つにまとめたものをコレクションと言うらしい。コレクションにも、モデルと同じようにイベントを登録したりすることが可能で、イベントの扱いがもっと簡単になるような概念です。 例えば、今まで人モデルにイベントを登録するには、一人一人にイベントを登録しなければいけませんでしたが、一人一人を一つのコレクションにまとめることによって、一括したイベント管理が行えるようになるということらしい。

Backbone.Collection.extend(properties, [classProperties])

モデルクラス同様、コレクションも独自のコレクションクラスを生成することができます。

collection.model

コレクションクラスには、モデルクラスを含めることが可能で、それを実現するにはcollection.modelをオーバーライドします。
var Book = Backbone.Model.extend({
  defaults: {
    key1: "1",
    key2: "2"
  },
  hoge: function () {
    console.log("hoge!");
  }
});

var Library = Backbone.Collection.extend({
  model: Book
});Code language: JavaScript (javascript)
また、modelを関数としてオーバライドすると、関数でモデルを生成することもできます。
var Library = Backbone.Collection.extend({

  model: function(attrs, options) {
    if (condition) {
      return new PublicDocument(attrs, options);
    } else {
      return new PrivateDocument(attrs, options);
    }
  }

});Code language: JavaScript (javascript)

constructor / initialize new Collection([models], [options])

Collectionのconstructorによる初期化では、モデルを渡すことができます。
var Book = Backbone.Model.extend({
    defaults: {
        name: "",
        price: "",
        author: "",
        genre: ""
    }
});

var books = new Backbone.Collection([
    [],
    [],
    [],
    []
  ], {
  model: Book
});

console.log(books.models[0].attributes);
// => Object {name: "", price: "", author: "", genre: ""}Code language: JavaScript (javascript)

collection.models

Collection内部への直接直接アクセスを行います。本来であればgetを使用するべきです。
console.log(books.models);Code language: CSS (css)

collection.toJSON()

これもモデルと同じように、JSON文字列にシリアライズ可能なハッシュを返します。 一見toJSONだけで文字列にできるように見えますができません。JSON.stringifyする際に暗黙的に呼ばれるので、あえて明示的に使用する必要はありません。
var collection = new Backbone.Collection([
  {name: "Tim", age: 5},
  {name: "Ida", age: 26},
  {name: "Rob", age: 55}
]);

console.log(JSON.stringify(collection));Code language: JavaScript (javascript)

collection.sync(method, collection, [options])

サーバーへモデルの状態を保持する Backbone.syncを使用するためのオプションです。

Underscore Methods (28)

Underscoreの基本的なメソッドをコレクションに対して使用することができます。_を使わなくても、コレクションのメソッドとしてデリゲート(委任)されているのでそのままコレクションに対して使用できます。
var books = new Backbone.Collection([
    {id:1, title: "aaa", group: "A", price: 1000},
    {id:2, title: "bbb", group: "B", price: 1980},
    {id:3, title: "ccc", group: "A", price: 980},
    {id:4, title: "ddd", group: "B", price: 498}
]);

// モデルを一つづつ反復処理をする
books.each(function (m) {
  console.log(m);
  // => a.Model, a.Model, a.Model, a.Model
});


// モデルのプロパティを利用しマッピングを行い配列を返す
books.map(function (m) {
  return [ m.get("id"), m.get("title") ];
});
// => [[1,"aaa"],[2,"bbb"],[3,"ccc"],[4,"ddd"]]

// モデルの指定したプロパティの数値を足して合計を出す ( nは前回値, mは現在のモデル, i はカウント, ms はモデルの配列, reduce第二パラメータは初期値)
books.reduce(function (n, m, i, ms) {
  n += m.get("price");
  return n;
}, 0);
// => 4458

// モデルを逆から反復処理をし値を取得していく
books.reduceRight(function (n, m, i, ms) {
  n += m.get("price");
  return n;
}, 0);
// => [498, 980, 1980, 1000]


// 条件式がtrueになる全ての値の最初のモデルのみを抽出します
books.find(function (m) {
  return m.get("group") == "A"
});
// => a.Model

// 条件式がtrueになる全てのモデルを抽出
books.filter(function (m) {
  return m.get("group") == "A"
});
// => [ a.Model, a.Model ]

// プロパティのキーと値のペアが一致するすべてのモデルの配列を返す
books.where({
  group: "B"
});
// => [ a.Model, a.Model ]


// プロパティのキーと値のペアが一致する全てのモデルの最初のモデルのみを抽出します
books.findWhere({
  group: "A"
});
// => a.Model

// 条件式でfalseになる全てのモデルを抽出
books.reject(function (m) {
  return m.get("group") == "A"
});
// => [ a.Model, a.Model ] ※ groupはBが抽出される

// コレクションの全てのモデルの条件式がtrueだった場合最終的にtrueを返す
books.every(function (m) {
  return m.get("group") == "A"
});
// => false

// コレクションの反復処理中に条件式でひとつでもtrueになった場合、最終的にtrueを返す
books.some(function (m) {
  return m.get("group") == "A"
});
// => true


// コレクションの中に指定したモデルが含まれているか調べる
books.contains(books.models[0]);
// => true


// 指定された関数によってコレクションを反復処理します。第二パラメータ移行の引数は関数の引数になります
books.invoke(function(c){
  c.log(this);
  return this;
}, console);


// プロパティ値の一覧を指定したキーから抽出する
books.pluck("title")
// => ["aaa", "bbb", "ccc", "ddd"]


// 反復処理の条件式のうちもっとも値の高いモデルを返す
books.max(function (m) {
  return m.get("price");
});

// 反復処理の条件式のうちもっとも値の低いモデルを返す
books.min(function (m) {
  return m.get("price");
});

// 値の低い順に並び替える
books.sortBy(function (m) {
  return m.get("price");
});
// => [ a.Model(498), a.Model(980), a.Model(1000), a.Model(1980) ]


// グループ分けする
books.groupBy(function (m) {
  return m.get("group");
});
// => Object {A: Array[2], B: Array[2]}


// 値を元にグループ分けします
books. countBy(function (m) {
  return m.get("group");
});
// => Object {A: 2, B: 2}


// モデルをシャッフルしたものを返します
books.shuffle();
// => [a.Model, a.Model, a.Model, a.Model]


// 配列にして返す
books.toArray();
// => [a.Model, a.Model, a.Model, a.Model]

// モデルの個数を調べる
books.size();
// => 4

// 先頭のモデルを返す
books.first();
// => a.Model



// 最後のモデルを除外した配列を返す
books.initial();
// => [a.Model, a.Model, a.Model]



// 最後の2モデルを除外した配列を返す
books.initial(2);
// => [a.Model, a.Model]


// 最後のモデルを返す
books.last();
// => a.Model


// 先頭のモデルを除外したものを返す
books.rest();
// => [a.Model, a.Model, a.Model]


// 先頭の2モデルを除外したものを返す
books.rest(2);
// => [a.Model, a.Model, a.Model]



// 特定のモデルを除外します
books.without(books.models[0]);
// => [a.Model, a.Model, a.Model]


// 特定のモデルが何番目にあるのか調べる(存在しない場合 -1 が返る)
books.indexOf(books.models[1]);
// => 1


// 特定のモデルが何番目にあるのか調べる。ただし、最後から走査する。(存在しない場合 -1 が返る)
books.lastIndexOf(books.models[1]);
// => 1

// 特定のモデルの特定のプロパティを元にコレクションをソートしたとき、その特定プロパティが何番目かを返す
books.sortedIndex(books.models[1], "price");
// => 4


// コレクションが空か調べる
books.isEmpty();
// => false

// モデルが空か調べる
books.isEmpty(books.models[0]);
// => false

// ラップされたオブジェクトを返します
books.chain();
// => j {_wrapped: Array[4], _chain: true, after: function, all: function, any: function…}



Code language: JavaScript (javascript)

collection.add(models, [options])

コレクションにモデルを追加します。modelsにはそのままインスタンスとして生成済みのモデルを渡すこともできますが、ハッシュをそのまま渡すことで、内部でモデルを生成して追加させることもできます。モデルを追加するとcollectionには add イベントが発生します。
var ships = new Backbone.Collection();

ships.on("add", function(ship) {
  console.log(ship.get("name")+" さんが追加されました");
});

ships.add([
  {name: "佐藤"},
  {name: "木村"},
  {name: "吉田"},
  {name: "吉田"}
]);

// => 佐藤 さんが追加されました
// => 木村 さんが追加されました
// => 吉田 さんが追加されました
// => 吉田 さんが追加されましたCode language: JavaScript (javascript)

collection.remove(models, [options])

指定したモデルをコレクションから削除します。 モデル削除時には、 remove イベントが collection に対して発生します。
var ships = new Backbone.Collection();

ships.on("add", function(ship) {
  console.log(ship.get("name")+" さんが追加されました");
});

ships.on("remove", function(ship) {
  console.log(ship.get("name")+" さんが消されました");
});

ships.add([
  {name: "佐藤"},
  {name: "木村"},
  {name: "吉田"},
  {name: "吉田"}
]);

// => 佐藤 さんが追加されました
// => 木村 さんが追加されました
// => 吉田 さんが追加されました
// => 吉田 さんが追加されました


ships.remove(ships.models[0]);
// => 佐藤 さんが消されましたCode language: JavaScript (javascript)

collection.reset([models], [options])

コレクションをハッシュを元に作り直します。実質的にリセットです。
var ships = new Backbone.Collection();

ships.add([
  {name: "佐藤"},
  {name: "木村"},
  {name: "吉田"},
  {name: "吉田"}
]);

ships.reset(ships.toJSON());Code language: JavaScript (javascript)

collection.set(models, [options])

新しいコレクションのハッシュをセットします。オプションにdelete: falseを指定すると、既存のモデルは削除されることはありません。merge:false merge:true add:false 等もあるようです。
// 新しいコレクションをセットしなおす
var ships = new Backbone.Collection([], {});
ships.add([
  {name: "佐藤"},
  {name: "木村"},
  {name: "吉田"},
  {name: "吉田"}
]);
ships.set([
  {name: "田村"},
  {name: "後藤"},
  {name: "樋口"},
  {name: "桑原"}
]);
// => 佐藤, 木村, 吉田, 吉田 削除され 田村, 後藤, 樋口, 桑原が追加される


// 削除せずに追加を行う
var ships = new Backbone.Collection([]);
ships.add([
  {name: "佐藤"},
  {name: "木村"},
  {name: "吉田"},
  {name: "吉田"}
]);
ships.set([
  {name: "田村"},
  {name: "後藤"},
  {name: "樋口"},
  {name: "桑原"},
  {name: "佐藤"},
  {name: "木村"},
  {name: "吉田"},
  {name: "吉田"}
], { remove:false });
// => a.Collection {length: 12, models: Array[12], _byId: Object, on: function, once: function…}
Code language: JavaScript (javascript)

collection.get(id)

コレクションからidまたはcidで指定されたモデルを取得します。
var ships = new Backbone.Collection([], {});
ships.add([
  {name: "佐藤"},
  {name: "木村"},
  {name: "吉田"},
  {name: "吉田"}
]);
ships.get("c396");
// => a.ModelCode language: JavaScript (javascript)

collection.at(index)

コレクションで、indexから指定されたモデルを取得します。
ships.at(1);
// => a.ModelCode language: JavaScript (javascript)

collection.push(model, [options])

コレクションの最後にモデルを追加します。
var ships = new Backbone.Collection([], {});
ships.add([
  {name: "佐藤"},
  {name: "木村"},
  {name: "吉田"},
  {name: "吉田"}
]);
ships.push({name: "室伏"});Code language: JavaScript (javascript)

collection.pop([options])

コレクションの最後からモデルを取り出して、コレクションから削除します。
var ships = new Backbone.Collection([], {});
ships.add([
  {name: "佐藤"},
  {name: "木村"},
  {name: "吉田"},
  {name: "吉田"}
]);
ships.pop();
// => a.Model(吉田)Code language: JavaScript (javascript)

collection.unshift(model, [options])

コレクションの先頭にモデルを追加します。戻り値に追加したモデルが返ります。
var ships = new Backbone.Collection([], {});
ships.add([
  {name: "佐藤"},
  {name: "木村"},
  {name: "吉田"},
  {name: "吉田"}
]);
ships.unshift({name: "前田"});
// => a.Model(前田)Code language: JavaScript (javascript)

collection.shift([options])

コレクションの先頭からモデルを取り出して、削除します。
var ships = new Backbone.Collection([], {});
ships.add([
  {name: "佐藤"},
  {name: "木村"},
  {name: "吉田"},
  {name: "吉田"}
]);
ships.shift();
// => a.Model(佐藤)Code language: JavaScript (javascript)

collection.slice(begin, end)

begin番目からend番目までのモデルを切り出したものを返します。(コレクションから削除されるわけではありません)
var ships = new Backbone.Collection([], {});
ships.add([
  {name: "佐藤"},
  {name: "木村"},
  {name: "吉田"},
  {name: "吉田"}
]);
ships.slice(2, 3);
// => a.Model(吉田)Code language: JavaScript (javascript)

collection.length

コレクションの保有しているモデルの数を返します。
var ships = new Backbone.Collection([], {});
ships.add([
  {name: "佐藤"},
  {name: "木村"},
  {name: "吉田"},
  {name: "吉田"}
]);
ships.length;Code language: JavaScript (javascript)

collection.comparator

コンパレータはデフォルトでは定義されていません。このコンパレータをオーバーライドし、コンパレータとして使用するキーを設定すると、そのキーによって自動的にモデルをソートしてくれるようになります。
// モデルクラスとコレクションインスタンスの定義
var Chapter  = Backbone.Model;
var chapters = new Backbone.Collection;


// コンパレータとして使用するキーを設定する
chapters.comparator = function(chapter) {
  return chapter.get("page");
};

// モデルの追加
chapters.add(new Chapter({page: 9, title: "The End"}));
chapters.add(new Chapter({page: 5, title: "The Middle"}));
chapters.add(new Chapter({page: 1, title: "The Beginning"}));


// titleキーの値を抽出
console.log(chapters.pluck('title'));Code language: JavaScript (javascript)

collection.sort([options])

明示的にこれを呼び出す必要はありません。ソート機能を使いたい場合、コンパレータによる自動ソートの使用をおすすめします。もし自動ソートをやめたい場合は、オプションに、{sort: false}を指定します。
// モデルクラスとコレクションインスタンスの定義
var Chapter  = Backbone.Model;
var chapters = new Backbone.Collection;


// ソートやめる
chapters.sort({sort: false});

// コンパレータとして使用するキーを設定する
chapters.comparator = function(chapter) {
  return chapter.get("page");
};
// => Error: Cannot sort a set without a comparator


// モデルの追加
chapters.add(new Chapter({page: 9, title: "The End"}));
chapters.add(new Chapter({page: 5, title: "The Middle"}));
chapters.add(new Chapter({page: 1, title: "The Beginning"}));

// titleキーの値を抽出
console.log(chapters.pluck('title'));Code language: JavaScript (javascript)

collection.pluck(attribute)

// コレクション作成
var stooges = new Backbone.Collection([
  {name: "Curly"},
  {name: "Larry"},
  {name: "Moe"}
]);

// name属性の値抽出
stooges.pluck("name");
// => ["Curly", "Larry", "Moe"]Code language: JavaScript (javascript)

collection.where(attributes)

渡された{key:value}を持つモデルを複数返します。
var friends = new Backbone.Collection([
  {name: "Athos",      job: "Musketeer"},
  {name: "Porthos",    job: "Musketeer"},
  {name: "Aramis",     job: "Musketeer"},
  {name: "d'Artagnan", job: "Guard"},
]);

friends.where({job: "Musketeer"});
// => [a.Model, a.Model, a.Model]Code language: JavaScript (javascript)

collection.findWhere(attributes)

渡された{key:value}を持つモデルで一番最初に見つかったモデルのみを返します。
var friends = new Backbone.Collection([
  {name: "Athos",      job: "Musketeer"},
  {name: "Porthos",    job: "Musketeer"},
  {name: "Aramis",     job: "Musketeer"},
  {name: "d'Artagnan", job: "Guard"},
]);

friends.findWhere({job: "Musketeer"});
// => a.Model(Musketeer)Code language: JavaScript (javascript)

collection.url or collection.url()

サーバー上のコレクションに結びつく独自のURLを設定します。
// プロパティとして指定
var Notes = Backbone.Collection.extend({
  url: '/notes'
});


// 関数を使用する場合
var Notes = Backbone.Collection.extend({
  url: function() {
    return this.document.url() + '/notes';
  }
});Code language: JavaScript (javascript)

collection.parse(response, options)

fetchでサーバーからのレスポンスを得る際に、得たデータをコレクションとして取り入れる前に、少し加工してからモデルに取り入れるための処理を行います。
// 同期処理
Backbone.sync = function(method, model) {

  console.log(method + ": " + JSON.stringify(model));

  // read の場合
  if (method == "read") {

    // モデルを追加 (データをサーバーから取得したと仮定)
    this.add(this.parse({data: {name: "test"}}));
  }
};

// コレクションクラス作成
var Tweets = Backbone.Collection.extend({
  parse: function(response) {
    console.log("parseが呼び出されました", response);
    return response["data"];
  }
});

// コレクションインスタンス生成
var t = new Tweets([], {parse: true});

// 受信開始
t.fetch();

console.log(t);
Code language: JavaScript (javascript)
urlを使用する場合はこのようにします。
Backbone.sync = function(method, model) {
  console.log(method + ": " + model.url);
};

var accounts = new Backbone.Collection;
accounts.url = '/accounts';

accounts.fetch();Code language: JavaScript (javascript)
何も削除せずに、追加と更新のみを行う処理をしたい場合はこのようにします。
collection.fetch({remove: false});Code language: CSS (css)
また、ページネーションを表すのに
documents.fetch({data: {page: 3}});Code language: CSS (css)
が使用できるそうですが、どう使えばいいのか不明。。

collection.create(attributes, [options])

新しくモデルを作成すると同時に、データベース上にも新しく作成したモデルデータを登録します。 なるほど便利。
Backbone.sync = function(method, model) {
  console.log(method + ": " + model.url);
};


var Book = Backbone.Model.extend({
    url: "/document",
});
var Library = Backbone.Collection.extend({
  url: "/document",
  model: Book
});

var nypl = new Library;
var othello = nypl.create({
  title: "Othello",
  author: "William Shakespeare"
});Code language: JavaScript (javascript)

Backbone.Router

長い…長すぎる。上から下へ眺めるだけなのに何時間だったのだろうか。サンプルスクリプトをちまちま書いてたからか、今一番見たくないのはスクロールバー。 それはさておき、いよいよルーティングです。ブラウザのページを動的に切り替えるためのロジックなのでかなり期待大!です。 本題へ移ります。 ルーティングというのはブラウザのページ切り替えのURLのことです。ページを切り替えるとURLが代わり、URLひとつひとつに割り当てられているページが違う。当たり前のことではありますが、このURLはこれ、このURLはこっちと、振り分けていくことをルーティングと言います。 これまでのWebサービスは、ページを切り替える度に画面がフラッシュし、DNS解決やHTTPリクエストを送ったりとページ切り替えに少なからずストレスを感じずにはいられませんでした。Twitterがhashchangeによって動的なページ切り替えを実現はしましたが、URLが「https://hogehoge.com/#!hoge」というようにhash=#を使わなければならず、URLとしては少し見栄えが悪いものでした。 そこで、History API というものが登場し、HTTPリクエストを発生させなくても、URLを書き換えられるようになりました。URLを書き換えられるということは、URLを書き換えるタイミングで、HTMLもJavaScriptを使って書き換えるようにすれば、クライアントにとってあたかもページが高速に切り替わった印象を与えることができるようになります。 それに、ajaxの技術を組み合わせることで、リッチなWebアプリケーションの開発を行うことができます。そんなことを実現させてくれるのが、Backbone.Routerです。 Backbone.Routerを使用するには、 ページのロードが完了したら Backbone.history.start(); または Backbone.history.start({pushState: true}); によって、ルーティング初期URLとするために呼び出してください。

Backbone.Router.extend(properties, [classProperties])

特定のURLフラグメントが一致したときに実行されるアクションを定義した ルーティングクラスを作成します。 先頭に / は必要ありません。
// カスタムルータクラスの生成
var WorkspaceRouter = Backbone.Router.extend({


  // ルーティング
  routes: {
    "help":                 "help",    // #help
    "search/:query":        "search",  // #search/kiwis
    "search/:query/p:page": "search"   // #search/kiwis/p7
  },

  //ヘルプアクション
  help: function() {
    console.log("ヘルプアクションです");
  },

  //サーチアクション
  search: function(query, page) {
    console.log("サーチアクションです");
  }

});Code language: JavaScript (javascript)

router.routes

このハッシュオブジェクトによってルーティングを行います。 :param の部分がパラメータになり、 *splat の部分が /hoge/hoge/hoge/ と構造を表すことができます。 ルーティング時、あってもなくても良いような指定を行う場合は (/:optional) と書きます。 例えば “search/:query/p:page” は search/obama/p2 にアクセスすることができ、:query にはobama、 :page には 2が入ります。 “file/*path” は file/nested/folder/file.txt としてアクセスできます。 具体的には、ルーティングは
routes: {
  "help/:page":         "help",
  "download/*path":     "download",
  "folder/:name":       "openFolder",
  "folder/:name-:mode": "openFolder"
}Code language: JavaScript (javascript)
のように書くことができ、
router.on("route:help", function(page) {
  ...
});Code language: JavaScript (javascript)
としてアクションを登録することもできます。

new Router([options])

オプションにハッシュを渡すことで初期化メソッドを実行することができます。
initialize: function(options) {

  // Matches #page/10, passing "10"
  this.route("page/:number", "page", function(number){ ... });

  // Matches /117-a/b/c/open, passing "117-a/b/c" to this.open
  this.route(/^(.*?)\\/open$/, "open");

},

open: function(id) { ... }Code language: JavaScript (javascript)

router.route(route, name, [callback])

手動でルーティングのルートを作成します。 router.routes による定義やアクションメソッドの定義はできません。
new Backbone.Router({
  

  // 初期化
  initialize: function(options) {

    this.route("page/:number", "page", function(number){});
    this.route(/^(.*?)\\/open$/, "open");

  },

  open: function(id) {
    console.log("openがよばれました");
  }

});Code language: JavaScript (javascript)

router.navigate(fragment, [options])

URLの変更とアクションの実行を行い、ページ遷移の挙動を振る舞います。
// カスタムルータクラスの生成
var RouterClass = Backbone.Router.extend({

  // ルーティング
  routes: {
    "help":                 "help",    // #help
    "search/:query":        "search",  // #search/kiwis
    "search/:query/p:page": "search"   // #search/kiwis/p7
  },

  //ヘルプアクション
  help: function() {
    console.log("ヘルプアクションです");
  },

  //サーチアクション
  search: function(query, page) {
    console.log("サーチアクションです");
  }

});


// ルーティングインスタンス生成
var router = new RouterClass();

// History API 開始
Backbone.history.start({pushState: true});

// ナビゲーション: helpアクション実行
router.navigate("help", {trigger: true});

// ナビゲーション: searchアクション実行
router.navigate("search", {trigger: true, replace: true});Code language: JavaScript (javascript)

Backbone.history

hashchange または pushState によるコールバック関数としての機能を果たします。 Hisory APIに対応していないブラウザの場合はhashchangeによるページ遷移を行います。

Backbone.history.start([options])

ルーティングが正しく設定してある場合において、 hashchangeイベントを監視するようにするには、 Backbone.history.start() を実行します。 HTML5のpushStateを利用したい場合は、 Backbone.history.start({pushState: true}); を指定します。 ルートURLを指定するには、 Backbone.history.start({pushState: true, root: “/public/search/”}) とします。 Backbone.history.start() は 定義されたルートURLが一致するとtrueを返し、一致しない場合はfalseを返します。 hashchangeの場合、iframeに依存しているので、DOMの生成が完了しおわってから実行するようにしてください。

Backbone.sync

Backbone.syncがサーバーからデータを読み込んだり、保存したりする関数です。デフォルトでは、RESTfulなJQueryなAjaxが呼び出されるようになっていますが、この関数をオーバーライドすることによって、WebSocketだったり、localStorageだったりと、データストアを自由に切り替えることができます。 オーバーライドするには、
// 同期処理
Backbone.sync = function(method, model) {
  console.log(method + ": " + JSON.stringify(model));
};Code language: JavaScript (javascript)
とすることで、自由に処理を設定できます。console.logにて、どのようなmethodやmodelが呼ばれているのかを確認することができます。modelには model.urlが割り当てられていることがあるので、model.urlからURLを取得し、そのURLに対して通信を行うこともできます。
  • method – the CRUD method (“create”, “read”, “update”, or “delete”)
  • model – the model to be saved (or collection to be read)
  • options – success and error callbacks, and all other jQuery request options
また、 モデルやコレクションがサーバーに通信を行うとき、 それぞれに対して、 request イベントが発生します。 RESTfulな実装の場合、各種HTTPメソッドはこのような割り当てになっています。
    create → POST /collection read → GET /collection[/id] update → PUT /collection/id delete → DELETE /collection/id

Backbone.ajax = function(request) { … };

ajaxをカスタマイズします。

Backbone.emulateHTTP = true

REST/HTTPをサポートしていない従来のWebサーバーを利用している場合、 Backbone.emulateHTTP を trueにすると、 X-HTTP-Method-Override header を付与し、 HTTPメソッドは _method に割り当てられます。
Backbone.emulateHTTP = true;

model.save();  // POST to "/collection/id", with "_method=PUT" + header.Code language: JavaScript (javascript)

Backbone.emulateJSON = true

Backbone.emulateJSON を trueにすると、 JSONとしてエンコードのリクエストを扱うことができない従来のWebサーバで application/x-www-form-urlencoded として取得するようになります。

Backbone.View

モデルに登録したイベントのchangeイベントを監視して、モデルに変更があるタイミングでテンプレートエンジンを利用して画面を再描画します。

Backbone.View.extend(properties, [classProperties])

カスタムビュークラスの作成を開始します。
// ビュークラスを作成
var DocumentRow = Backbone.View.extend({

  tagName: "li",

  className: "document-row",

  events: {
    "click .icon":          "open",
    "click .button.edit":   "openEditDialog",
    "click .button.delete": "destroy"
  },

  initialize: function() {
    this.listenTo(this.model, "change", this.render);
  }

  render: function() {
  }

});
Code language: JavaScript (javascript)
eventsには、
  • tagName
  • id
  • className
  • el
  • event
が定義できます。

constructor / initialize

ビューインスタンスの生成を行います。
// ビュークラスを作成
var DocumentRow = Backbone.View.extend({

  tagName: "li",

  className: "document-row",

  events: {
    "click .icon":          "open",
    "click .button.edit":   "openEditDialog",
    "click .button.delete": "destroy"
  },

  initialize: function() {
    this.listenTo(this.model, "change", this.render);
  }

  render: function() {
  }

});

// モデル作成
var model = new Backbone.Model({
    id: 1
    name: "山田"
});


// ビューインスタンス生成
new DocumentRow({
  model: model,
  id: "document-row-" + model.id
});Code language: JavaScript (javascript)

view.el

ここに生成済みのdomが格納されます。
var ItemView = Backbone.View.extend({
  tagName: 'li'
});

var BodyView = Backbone.View.extend({
  el: 'body'
});

var item = new ItemView();
var body = new BodyView();

console.log(item.el + ' ' + body.el);Code language: JavaScript (javascript)

view.$el

DOM要素がjQueryでラッピングされたものです。
view.$el.show();

listView.$el.append(itemView.el);Code language: PHP (php)

view.setElement(element)

別のDOM要素にViewを適用したい場合に使用します。

view.attributes

view.elの属性のハッシュオブジェクトらしいが、undefinedしか帰ってこないため不明。

view.$(selector)

view.$で jQueryを利用することができます。
ui.Chapter = Backbone.View.extend({
  serialize : function() {
    return {
      title: this.$(".title").text(),
      start: this.$(".start-page").text(),
      end:   this.$(".end-page").text()
    };
  }
});Code language: JavaScript (javascript)

view.render()

デフォルトでは何もしませんが、this.elによって生成されたHTMLとテンプレートとモデルデータをレンダリングするようにこの関数をオーバーライドします。 レンダリング終了時にこの関数を呼び出すと良いです。
var Bookmark = Backbone.View.extend({
  template: _.template(…),
  render: function() {
    this.$el.html(this.template(this.model.attributes));
    return this;
  }
});Code language: JavaScript (javascript)
ここではアンダースコアのテンプレートエンジンを使用していますが、 Mustache.js や Haml-js など様々なテンプレートエンジンがあるので、使ってみることをおすすめします。

view.remove()

ビューとビューの監視イベントを削除します。

delegateEvents([events])

var DocumentView = Backbone.View.extend({

  events: {
    "dblclick"                : "open",
    "click .icon.doc"         : "select",
    "contextmenu .icon.doc"   : "showMenu",
    "click .show_notes"       : "toggleNotes",
    "click .title .lock"      : "editAccessLevel",
    "mouseover .title .date"  : "showTooltip"
  },

  render: function() {
    this.$el.html(this.template(this.model.attributes));
    return this;
  },

  open: function() {
    window.open(this.model.get("viewer_url"));
  },

  select: function() {
    this.model.set({selected: true});
  },

  ...

});Code language: JavaScript (javascript)

undelegateEvents

ビューに委任されている全てのイベントを削除します。

サンプルスクリプト

よくわからなかったのでサンプルスクリプトを書いてみました。
// ビュークラスを作成
var DocumentView = Backbone.View.extend({

  // tenplate
  template: _.template('<div><%= name %><div class="icon">開く</div><div class="button edit">編集</div><div class="button delete">削除</div></div>'),

  // イベント
  events: {
    "click .icon":          "open",
    "click .button.edit":   "openEditDialog",
    "click .button.delete": "destroy"
  },

  // 初期化
  initialize: function() {
    console.log(this.model, "を監視します");
    this.listenTo(this.model, "change", this.render);
    this.render();
  },

  // 描画
  render: function() {
    console.log("描画を行います");
    this.$el.html(this.template(this.model.attributes));
    $(document.body).html("");
    $(document.body).append(this.$el);

  },

  open: function () {
    alert("open");
  },

  openEditDialog: function () {
    alert("openEditDialog");
  },

  destroy: function () {
    alert("destroy");
  }

});

// モデル作成
var model = new Backbone.Model({
    id: 1,
    name: "山田"
});


// ビューインスタンス生成
var view = new DocumentView({
  model: model,
  id: "document-row-" + model.id
});Code language: JavaScript (javascript)
モデルの変更によって動的にビューが書き換わっているはずなのに、イベントが死なない… すごい… また、サンプル中では、テンプレートを一つしか使ってないので、これをうまくいろんなテンプレートを使えるようにオブザーバを工夫する必要があります。

Utility

var backbone = Backbone.noConflict();

既存のbackboneオブジェクトを破壊せずに、もう一つbackboneを作成して使用します。
var localBackbone = Backbone.noConflict();
var model = localBackbone.Model.extend(...);Code language: JavaScript (javascript)

Backbone.$ = $;

jQueryのエイリアスです。

最後に

ようやく下まで眺め終わりました。眺めるレベルじゃない気もしますが、とりあえず….眠いです。おやすみなさい。

テストとjQueryのeachっぽい何かを書いてみた

jQueryは便利だけど、わざわざjQueryをロードせずともできるだけ同じようなことがしたいというときに便利。jQueryほどセレクタの指定ができないが、forEachでぐるぐると実行してくれるので便利。おまけにテストも書いてみました。 使う場面としたら、Chrome拡張やブックマークレットくらいでしょうか。
<script>


/* スクリプト
-------------------------------------------------------------------------------*/
var tags, classes, id;
(function(){
    var a = function(o){return Array.prototype.slice.apply(o)},
    d = document,
    g = function(t){return d.getElementsByTagName(t)};
 
    tags = function(t, f, h) {
        var c = 0;
        a((h == undefined ? g(t) : h.getElementsByTagName(t))).forEach(function(h) {
            f.call(h, c);
            c++;
        });
    };
 
 
    classes = function(l, f, h) {
        var o = 0;
        a((h == undefined ? g("*") : h.getElementsByTagName("*"))).forEach(function(h) {
            var c = h.className.split(" ");
            for (var i = 0; i < c.length; i++) {
                if (c[i] == l) {
                    f.call(h, o);
                    o++;
                    break;
                }
            };
        });
    };
 
    id = function(i, f) {
        f.call(d.getElementById(i));
    };
}).call(this);



/* テストスクリプト
-------------------------------------------------------------------------------*/

var testResult = "";
var print = function(_print) {
    testResult += _print;
};

var testData = function() {
    document.write(new Array(100).join("-")+"<br/>");
    document.write("テストデータ<br/>");
    document.write(new Array(100).join("-")+"<br/><br/>");
};



var testLog = function(){
    document.write("<br/><br/>");
    document.write(new Array(100).join("-")+"<br/>");
    document.write("テスト結果<br/>");
    document.write(new Array(100).join("-")+"<br/>");
    document.write(testResult);
};


var suite = function(_suite) {
    print("<br/>■ "+_suite+"<br/>");
};

var test = function(_name){

    var result = 1;
    for (var i = 1; i < arguments.length; i++) {
        if (typeof(arguments[i]) == "function") {
            try {
                result = result && arguments[i]();    
            }
            catch (_error) {
                result = 0;
                throw (_error);
            }
            break;
        }
    };

    print((result ? "○" : "×")+" ");
    print(_name);
    print("<br/>");
};


testData();
</script>


<!-- テスト用データ
=============================================================================== -->
<meta charset="UTF-8">
<div>hoge</div>
<div class="hoge">hoge</div>
<div>hoge</div>
<div id="fufu">hoge</div>
<div class="hage hoge test">
    <span>aaa</span>
    <span>aaa</span>
    <span>aaa</span>
    <span>aaa</span>
</div>
<div class="test">
    <span>bbb</span>
    <span>bbb</span>
    <span>bbb</span>
    <span class="test">bbb</span>
    <span class="test">bbb</span>
    <span>bbb</span>
    <span>bbb</span>
</div>


<script>

/* テスト実行
-------------------------------------------------------------------------------*/

suite("tags");

test("ちゃんとtagsが実行された", function(){
    var count = 0;
    tags("div", function(){
        count++;
    });
    return count == 6;
});

test("インデックスが一致している", function(){
    var count = 0;
    var flag = 1;
    tags("div", function(i){
        count++;
        if (count == i) {
            flag = 0;
        }
    });
    return flag;
});

test("二階層目のspanを取得できる", function(){
    var count = 0;
    tags("div", function(i){
        if (i == 5) {
            tags("span", function() {
                count++;
            }, this);
        }
    });
    return count == 7;
});

suite("classes");

test("classesが実行できた", function(){
    var count = 0;
    classes("hoge", function(){
        count++;
    });
    return count == 2;
});

test("インデックスが一致している", function(){
    var count = 0;
    var flag = 1;
    classes("hoge", function(i){
        count++;
        if (count == i) {
            flag = 0;
        }
    });
    return flag;
});

test("カウンタが数値である", function(){
    var flag = 1;
    classes("hoge", function(i){
        if (typeof(i) != "number") {
            flag = 0;
        }
    });
    return flag;
});

test("二階層目のclassを取得できている", function(){
    var count = 0;
    classes("test", function(i){
        if (i == 1) {
            console.log(this);
            classes("test", function() {
                console.log(this);
                count++;
            }, this);
        }
    });
    return count == 2;
});

suite("id");

test("idが実行できた", function(){
    var count = 0;
    id("fufu", function(){
        count++;
    });
    return count == 1;
});

/* テスト結果出力
-------------------------------------------------------------------------------*/
testLog();

</script>

Code language: JavaScript (javascript)

こんなかんじでブックマークレットも作れる
var tags,classes,id;(function(){var a=function(o){return Array.prototype.slice.apply(o)},d=document,g=function(t){return d.getElementsByTagName(t)};tags=function(t,f,h){var c=0;a(h==undefined?g(t):h.getElementsByTagName(t)).forEach(function(h){f.call(h,c);c++})};classes=function(l,f,h){var o=0;a(h==undefined?g("*"):h.getElementsByTagName("*")).forEach(function(h){var c=h.className.split(" ");for(var i=0;i<c.length;i++)if(c[i]==l){f.call(h,o);o++;break}})};id=function(i,f){f.call(d.getElementById(i))}}).call(this);
tags("td",function(){var that=this;tags("a",function(){if(this.innerHTML.indexOf("\\u4ed9\\u53f0")+1)that.style.background="orange"},this)});Code language: JavaScript (javascript)

jQuery UI も Twitter Bootstrapもテーマカスタマイズができた

jQuery UI や Twitter Bootstrapのテーマを自由に変える

jQuery UI やTwitter Bootstrapのスタイルはcssによって描画されていますが、そのCSS(と画像ファイル)を自由にカスタマイズできるページがあったみたいです。

いやはやもう少し早く気づいておくべきでした・・。

jQuery UI

20120802040246

jQuery UI ThemeRoller
http://jqueryui.com/themeroller/

左ペインのメニューからいろいろ設定を変更してテーマを変えていきます。ギャラリーにはテーマ一覧もあったりして便利。設定完了したら「Download theme」ボタンをクリック。

Twitter Bootstrap

20120802040347

StyleBootstrap.info

http://stylebootstrap.info/

ここも同じようにカスタマイズした後最後にテーマをダウンロードできる。ダウンロードボタンがページ一番下にあるのでわかりにくいです。