試して学ぶ Backbone.js入門3
はじめに
前回の「試して学ぶ Backbone.js入門2」ではBackbone.jsの中心となるModelとCollectionの非同期RESTful JSONインタフェースについて説明しました。
今回はBackbone.jsによるアプリケーションのアーキテクチャを決定づけるBackbone.Eventsについて説明します。
今回のソースの全体は こちらで確認することができます。
Backbone.Events
Backbone.jsが提供するコンポーネントであるModel、Collection、View、Routerは全てBackbone.Events(以降、Events)の機能を継承しています。
Eventsが提供するイベントの監視設定(on, lisetenToメソッド)と解除(off, stopListeningメソッド)、イベントの発火(triggerメソッド)を利用することにより、コンポーネント間の依存関係を疎結合にすることができます。
もう少し具体的に説明すると、アプリケーションの中で、ModelやCollectionはデータを表現するコンポーネントですが、これらは画面制御を担うコントローラであるViewに依存すべきではありません。
そこで、このEventsの仕組みを利用することで、ModelやCollectionの変更をイベントとして通知することで、ModelやCollectionは直接Viewを知る必要がなく、また、View側は監視するイベントが発生したら、必要な処理を行うという一定のパターンで実装することができます。
イベントの監視設定/解除
Backbone.jsの標準イベントは公式ドキュメントのCatalog of Eventsに記載されています。
ここからはこれらのイベントを実際に監視してみましょう。
イベントの監視設定/解除の方法は2通りあります。1つ目はon(bind)/off(unbind)を利用する方法です。
こちらはイベントを発生させる側のオブジェクトに対して、対象のイベントと、通知先となるコールバックメソッドを指定します。
model.on( event, callback, [context] );
eventにはCatalog of Eventsにある通り、”add”、”change”、”all”のようにイベント名を指定します。
また、以下のようにevent mapとして定義することも可能です。
model.on( { event1: callback1, event2: callback2, event3: callback3 } );
onというメソッドには別名としてbindが定義されており、bindとして書くことができます。
イベントの監視設定を解除する場合は、off(unbind)を使います。
詳細はこちらを確認してください。
2つ目のlistenToとstopListeningを利用する方法です。
こちらはイベントを監視する側のオブジェクトに対して、イベントを発生させるオブジェクトと、対象のイベント、通知先となるコールバックメソッドを指定します。
observer.listenTo( target, event, callback );
イベントの監視設定を解除する場合は、stopListeningを使います。
詳細はこちらを確認してください。
この2つの方法の違いは、焦点をイベントを発行する側に持つか、イベントを監視する側に持つか、という違いになります。
イベントを発行する側に監視側を意識させるよりも、イベントを監視する側で、監視設定/解除を行うlistenTo/stopListeningを使う方がよいと個人的には思います。
実際にイベントを監視してみる(add, change, remove, reset, sort)
それでは実際に動かしながらイベントを監視してみましょう。前回までに取り上げたModelとCollectionを使って試してみます。
var Memo = Backbone.Model.extend({ idAttribute:"_id", defaults:{ "content":"" }, validate:function (attributes) { if (attributes.content === "") { return "content must be not empty."; } } }); var MemoList = Backbone.Collection.extend({ model:Memo, url:"/memo" }); var memoList = new MemoList();
ModelとCollectionの定義はこれまと同じです。
今回Modelにvalidationメソッドを定義していますが、これは後ほど説明します。
次に作成したModelとCollectionが発行するイベントを監視するための監視用オブジェクトを作成します。
var observer = { showArguments:function () { console.log("+++observer.showArguments: "); _.each(arguments, function (item, index) { console.log(" +++arguments[" + index + "]: " + JSON.stringify(item)); }); } }; _.extend(observer, Backbone.Events); observer.listenTo(memoList, "all", observer.showArguments);
監視用オブジェクトとしてobserverを作成し、コールバックメソッドとしてshowArgumentsを定義します。
これによりイベントが通知された際に渡される引数をコンソールに出力します。
observerオブジェクトにBackbone.Eventsの機能を持たせるようにUnderscoreのextendメソッドにより機能を継承させます。
そして、Collectionに対して全てのイベントを監視し、コールバック時にshowArgumentsメソッドを呼び出すように定義しています。
Collectionに対してイベントを監視した場合、Collectionに格納されているModelがイベントを発行した場合、そのイベントは親であるCollectionからも発行されるため、上記のように書くことでCollectionとModelのイベントを監視することができます。
それでは実際にイベントがどのように発生するのか確認してみます。
var memo = new Memo({content:"Acroquest"}); console.log("add"); memoList.add(memo); console.log("change"); memo.set({content:"Acroquest Technology"}); console.log("remove"); memoList.remove(memo); console.log("reset"); memoList.add([new Memo({content:"Acro1"}),new Memo({content:"Acro2"}),new Memo({content:"Acro3"})]); console.log("Before reset: " + JSON.stringify(memoList)); memoList.reset([new Memo({content:"Acro"}), new Memo({content:"Technology"}), new Memo({content:"Acroquest"})]); console.log("After reset: " + JSON.stringify(memoList)); console.log("sort"); memoList.comparator = function (item) { return item.get("content"); }; memoList.sort(); console.log("After sort: " + JSON.stringify(memoList)); observer.stopListening();
コンソール出力の結果から、実際に発生するイベントと渡される引数について確認してみましょう。
コンソール出力結果
add +++observer.showArguments: +++arguments[0]: "add" +++arguments[1]: {"content":"Acroquest"} +++arguments[2]: [{"content":"Acroquest"}] +++arguments[3]: {} change +++observer.showArguments: +++arguments[0]: "change:content" +++arguments[1]: {"content":"Acroquest Technology"} +++arguments[2]: "Acroquest Technology" +++arguments[3]: {} +++observer.showArguments: +++arguments[0]: "change" +++arguments[1]: {"content":"Acroquest Technology"} +++arguments[2]: {} remove +++observer.showArguments: +++arguments[0]: "remove" +++arguments[1]: {"content":"Acroquest Technology"} +++arguments[2]: [] +++arguments[3]: {"index":0} reset Before reset: [{"content":"Acro1"},{"content":"Acro2"},{"content":"Acro3"}] +++observer.showArguments: +++arguments[0]: "reset" +++arguments[1]: [{"content":"Acro"},{"content":"Technology"},{"content":"Acroquest"}] +++arguments[2]: {"previousModels":[{"content":"Acro1"},{"content":"Acro2"},{"content":"Acro3"}]} After reset: [{"content":"Acro"},{"content":"Technology"},{"content":"Acroquest"}] sort +++observer.showArguments: +++arguments[0]: "sort" +++arguments[1]: [{"content":"Acro"},{"content":"Acroquest"},{"content":"Technology"}] +++arguments[2]: {} After sort: [{"content":"Acro"},{"content":"Acroquest"},{"content":"Technology"}]
実際にイベントを監視してみる(request, sync, destroy, invalid)
それでは次は、RESTful JSONインタフェースに関連するイベントを監視してみましょう。
console.log("request, sync"); memo = new Memo({content:"Murata"}, {collection:memoList}); console.log("create"); memo.save(null, { success:function () { console.log("After create memoList: " + JSON.stringify(memoList)); console.log("After create memoList.length: " + memoList.length); } }).pipe(function () { console.log("fetch"); return memoList.fetch({ success:function () { console.log("After fetch memoList: " + JSON.stringify(memoList)); console.log("After fetch memoList.length: " + memoList.length); } }); }).pipe(function () { var tempMemo = memoList.find(function (item) { return item.get("content") === "Murata"; }); console.log("invalid"); tempMemo.save({content:""}); console.log("invalid wait:true"); tempMemo.save({content:""}, {wait:true}); console.log("re-save"); return tempMemo.save({content:"Kenichiro"}, { success:function () { console.log("After save memoList: " + JSON.stringify(memoList)); console.log("After save memoList.length: " + memoList.length); } }); }).done(function () { console.log("destroy"); var tempMemo = memoList.find(function (item) { return item.get("content") === "Kenichiro"; }); return tempMemo.destroy({ success:function () { console.log("After destroy memoList: " + JSON.stringify(memoList)); console.log("After destroy memoList.length: " + memoList.length); } }); }); memoList.add(memo); console.log("After add memo: " + JSON.stringify(memo)); console.log("After add memoList.length: " + memoList.length);
コードは前回までと同じです。
1点追加している部分はvalidationを確認する部分です。
Modelにはvalidationというメソッドがあり、save処理時にvalidationメソッドによってバリデーションエラーの有無をすることができます。
エラーが発生した場合にはサーバ側へのリクエストは行わず、invalidイベントを発行します。
optionsハッシュにwait:trueを指定しない場合(デフォルト動作)、プロパティの変更が先に行われますが、wait:trueを指定した場合は、validationによるチェックがエラーになる場合は、プロパティの変更は行いません。
それでは 出力を確認してみましょう。
コンソール出力結果
request, sync create +++observer.showArguments: +++arguments[0]: "add" After add memo: {"content":"Murata"} After add memoList.length: 1 +++observer.showArguments: +++arguments[0]: "change:__v" +++observer.showArguments: +++arguments[0]: "change:date" +++observer.showArguments: +++arguments[0]: "change:_id" +++observer.showArguments: +++arguments[0]: "change" After create memoList: [{"content":"Murata","__v":0,"date":"2013-02-21T13:04:54.036Z","_id":"51261b76ef23b0a066000001"}] After create memoList.length: 1 +++observer.showArguments: +++arguments[0]: "sync" fetch +++observer.showArguments: +++arguments[0]: "request" +++observer.showArguments: +++arguments[0]: "reset" After fetch memoList: [{"date":"2013-02-21T13:04:54.036Z","content":"Murata","_id":"51261b76ef23b0a066000001","__v":0}] After fetch memoList.length: 1 +++observer.showArguments: +++arguments[0]: "sync" invalid +++observer.showArguments: +++arguments[0]: "change:content" +++observer.showArguments: +++arguments[0]: "change" +++observer.showArguments: +++arguments[0]: "invalid" +++arguments[1]: {"date":"2013-02-21T13:04:54.036Z","content":"","_id":"51261b76ef23b0a066000001","__v":0} +++arguments[2]: "content must be not empty." +++arguments[3]: {"validate":true} invalid wait:true +++observer.showArguments: +++arguments[0]: "invalid" +++arguments[1]: {"date":"2013-02-21T13:04:54.036Z","content":"","_id":"51261b76ef23b0a066000001","__v":0} +++arguments[2]: "content must be not empty." +++arguments[3]: {"validate":true,"wait":true} re-save +++observer.showArguments: +++arguments[0]: "change:content" +++observer.showArguments: +++arguments[0]: "change" +++observer.showArguments: +++arguments[0]: "request" +++observer.showArguments: +++arguments[0]: "change:date" +++observer.showArguments: +++arguments[0]: "change" After save memoList: [{"date":"2013-02-21T13:04:54.309Z","content":"Kenichiro","_id":"51261b76ef23b0a066000001","__v":0}] After save memoList.length: 1 +++observer.showArguments: +++arguments[0]: "sync" destroy +++observer.showArguments: +++arguments[0]: "request" +++observer.showArguments: +++arguments[0]: "remove" +++observer.showArguments: +++arguments[0]: "destroy" After destroy memoList: [] After destroy memoList.length: 0
上記は長くなるので一部省略しています。
ぜひ実際に動作させてみて、イベント発行のタイミングと渡される引数について確認してみてください。
以下にコンソール出力から分かることを簡単にまとめます。
- Collectionのイベントを監視する場合、ModelがaddされなければModelのイベント発行を受けられない
- changeイベントはchange:attribute => changeイベントの順序となり、その後にsyncイベントが発生する
- fetchした場合、request => reset => syncイベントの順となる
- saveをwait:false(デフォルト)で呼び出した場合、指定したプロパティ(content)のchange:content => changeイベントが発生し、その後、サーバ側での変更を受けてchange:date => changeイベントが発生する
- destroyした場合、request => remove => destroyイベントの順で発生する
optionsハッシュのsilent:trueとwait:trueについて
最後に、関連する話題としてoptionsハッシュのslientオプションとwaitオプションについて説明します。
silentオプションは以下のメソッドに指定することができ、trueを指定した場合、イベントの発行をしなくなります(未指定の場合はfalseの扱いとなります)。
- Model.set, unset, clear
- Collection.add, remove, reset, sort
例えばCollectionにModelを複数回addする場合に、最後のaddの場合のみイベントを発行させる場合などに利用します。
waitオプションは以下のメソッドに指定することができ、trueを指定した場合、処理の反映がサーバにリクエストが成功した後になります(未指定の場合はfalseの扱いとなります)。
- Model.save, destroy(Collectionからの削除がサーバへのリクエスト成功後になる)
- Collection.create(Collectionへの追加がサーバへのリクエスト成功後になる)
次回
今回は第3回目として、Backbone.Eventsについて説明しました。次回はBackbone.Viewについて取り上げます。
著者: 村田賢一郎
Acroquest Technology 株式会社勤務。Javaによるミッションクリティカルな集中監視システムのフレームワーク開発、およびライフラインを支えるシステム開発に携わる。非同期処理、メッセージング、HAなどが本業である傍ら、Webによる新しいUI表現、開発手法に興味があり、あれこれ模索している。