Backbone.jsに入門してみる【サーバー通信編 (Model, Collection)】
今回はBackbone.ModelとBackbone.Collectionを使って
サーバーサイドとRESTな通信をしてみます。
サーバーとの通信ですが、別に難しいことはありません。
jQueryでいう $.get(), $.post() をBackbone.js風に使うだけです。
はじめに
サーバーとの通信ですが、今回は/usersというURLに対して行うことにします。
サーバー側はクライアント側からのRESTメソッドに対応した実装がされている前提です。
/usersというURLは名前からuserの集合を表していることがわかるかと思いますが、
このuserの集合というリソースに対してクライアント側から
任意のリソースを要求したり、リソースを作成したりできるのがRESTなAPIです。
この/usersに対してそれぞれのメソッドでリクエストした場合、
以下のような意味になります。
サーバー側ではそれぞれの意味になるように実装する必要があります。
- [GET] /users
- userの集合を取得する
- [POST] /users
- userを新規作成する
- [GET] /users/1
- user_id: 1のuserを取得する
- [PUT] /users/1
- user_id: 1のuserの情報を更新する
- [DELETE] /users/1
- user_id: 1のuserの情報を削除する
1と2は集合に対する操作なので、Backbone.Collectionで実装するのが良いでしょう。
3〜5は単一のリソースに対する操作なのでBackbone.Modelで実装するのが良いかもしれません。
urlプロパティ
サーバーと通信するためにはBackbone.Model, Backbone.Collectionのオブジェクトを
定義する時にurlプロパティを定義します。
通信先を書くだけなので、簡単ですね。
Backbone.Collectionを使って通信する場合
まずはサーバーからどんなレスポンスが返ってくるのか、
取り決めておかなければなりません。
今回は /users にGETでリクエストを送ったら下記のようなJSONが返ってくることとします。
{ "total": 3, "list": [ { id: 1, name: "太郎" }, { id: 2, name: "二郎" }, { id: 3, name: "三郎" } ] }
listにuser一人を表すオブジェクトが3つ入ってるだけの簡単なものです。
idはサーバー側でユニークなものと保証されていることにします。
Backbone.Modelを定義
user一人あたりの情報を定義します。
var UserModel = Backbone.Model.extend({ url : 'users/', // リソースへのパスを記述 initialize : function initialize() { // インスタンス生成時に実行される if (this.id) { this.url = this.url + this.id; } } });
urlプロパティ
リソースへのパスを記述します。
単一のリソースを示す場合はリソース名の後にIDを指定するため、
users/とスラッシュを後ろに付けてます。
initializeメソッド
new UserModel()とインスタンスを生成する時に呼ばれます。
if文ではこのModelにid属性が定義されているかどうかを識別して、
id属性があればurlプロパティにそのid(user_id)を追加するようにしています。
Backbone.Collectionを定義
userの集合を取得するためのCollectionを定義します。
var UserCollection = Backbone.Collection.extend({ model : UserModel, // このCollectionのBackbone.Modelを指定 url : 'users', // リソースへのパスを記述。http://〜と書いてもok parse : function parse(res) { // modelにsetする値を指定する return res.list; } });
modelプロパティ
このCollectionに属するBackbone.Modelオブジェクトを指定します。
urlプロパティ
リソースへのパスを指定します。
parseメソッド
ここがミソだったりします。
Backbone.Collectionのparseメソッドはfetch()を実行した時に
レスポンスを舐めてくれるメソッドです。
デフォルトだとparseメソッドはレスポンスをそのまま返すだけの
メソッドなのですが、このままだとuserModelに意図したデータが入りません。
そこでModelに入れるデータだけをparseメソッドで返すようにします。
この場合はレスポンスのlistプロパティがuser情報を示しているので、
これをreturnします。
ちなみにreturnする値はオブジェクトの配列じゃないと正しくイテレートできません。
実際に通信してみる
その前に、GETやPOSTで通信できているか確認するときはChromeの
デベロッパーツールを起動して、Networkタブを選択して、
All→XHRを選んでおくと外部通信がわかりやすく見れます。
Collection.fetch()
まずはCollectionのfetch()を実行してみます。
var userCollection = new UserCollection({}); userCollection.fetch({ success : function success(collection, res, options) { // 通信成功時の処理 }, error : function error() { // 通信失敗時の処理 } });
これだけでuserCollectionに3件のuserModelが追加されました。
実際には通信が完了したら〜〜したいってことがあるので、
var UserCollection = Backbone.Collection.extend({ model : UserModel, url : 'users', initialize : function initialize() { // 追記 this.listenTo(this, 'onFetch', this._onFetch); // イベント購読 }, parse : function parse(res) { return res.list; }, _onFetch : function _onFetch() { // 追記 // fetch()が終わった後の処理を書く } }); var userCollection = new UserCollection({}); userCollection.fetch({ success : function success(collection, res, options) { collection.trigger('onFetch'); // 追記:イベント発火 // userCollection.trigger('onFetch'); // これでもok }, error : function error() { // 通信失敗時の処理 } });
こんなかんじでメソッドを関連付けておくと良さそうです。
Collection.create()
createメソッドはリソースを新規作成するメソッドです。
使い所としては、ユーザーアカウントの新規作成など、
リソースを新しく登録したい場合などです。
create()メソッドの引数にCollection.modelに渡す属性を指定します。
var UserCollection = Backbone.Collection.extend({ model : UserModel, url : 'users', initialize : function initialize() { this.listenTo(this, 'add', this._onAdd); // 追記:イベント購読 }, parse : function parse(res) { return res.list; }, _onAdd : function _onAdd(model, collection, options) { // 追記 // Collectionにmodelが追加された時の処理を書く } }); var userCollection = new UserCollection(); userCollection.create({ name: 'hogehoge' }); // 追記: create()実行
createメソッドを実行するとCollection.urlに指定したURLにPOSTで通信されます。
サーバーサイドではこのURLにPOSTで通信があった場合はリソースを新規作成する
という実装を行い、DBなどに追加したらそのリソースをJSON形式で返しましょう。
collectionにmodelが追加されると'add'イベントが発火されます。
この'add'イベントを購読して、追加されたら_onAdd()メソッドが実行されるように
しておきました。
上記の記述だとサーバーのレスポンスが返る前にcollectionにmodelが追加されます。
もし厳密にサーバーからの応答を待ってからcollectionにmodelを追加する場合は
create()メソッドの第二引数にオプションで { wait: true } を渡して下さい。
// こんなかんじで userCollection.create({ name: 'hogehgoe' }, { wait: true });
Collection.save()
save()メソッドは単一のリソースを更新するメソッドです。
CRUDでいうところのUpdateです。通信はPUTで行われます。
var UserCollection = Backbone.Collection.extend({ model : UserModel, url : 'users', initialize : function initialize() { this.listenTo(this, 'onUpdate', this._onUpdate); // 追記:イベント購読 }, parse : function parse(res) { return res.list; }, _onUpdate : function _onUpdate(model) { // collection内のmodelが更新された時の処理を書く } }); var userCollection = new UserCollection(); userCollection.fetch({ success : function success(collection, res, options) { var userModel = collection.models[0]; // 1番目のmodelをとりあえず取得 collection.save({ name: 'hogehoge' }, { // nameをhogeに更新する success : function success(model, res, options) { // 更新が完了した時の処理を書く userCollection.trigger('onUpdate', model); // collectionをイベント発火 } }); } });
構文はCollection.create()と似ていると思います。
あるmodelがupdateされたというイベントはない(?)ようので、
とりあえずcollectionから'onUpdate'というイベントを発火させてます。
でも外部通信した結果、collectionの中身に変化があったら何か実行したい
という場合は'sync'イベントを購読して何かするっていうのがスマートかもしれません。
ちなみに 'all' というイベントを購読すると、どんなイベントが発火しているか
デバッグしやすいです。
initialize : function initialize() { // デバッグ用の追記 this.listenTo(this, 'all', this._debug); }, _debug : function _debug() { console.log(arguments); }
何も属性がセットされていないmodelに対してCollection.save(attributes)を行うと
POSTで通信されます。これは新しいリソースを新規作成しているのと同じなためです。
裏側の通信のメソッドが違うだけであり、クライアントサイドでは気にしなくてもよいです。
Collection.destroy()
destroy()メソッドは単一のリソースを削除するメソッドです。
通信はDELETEで行われます。
削除したいmodelを取得して、そのmodelのdestroy()メソッドを読んで実行します。
var UserCollection = Backbone.Collection.extend({ model : UserModel, url : 'users', initialize : function initialize() { this.listenTo(this, 'onFetch', this._onFetch); // イベント購読 this.listenTo(this, 'onDelete', this._onDelete); // 追記: イベント購読 }, parse : function parse(res) { return res.list; }, _onFetch : function _onFetch() { // fetch()が終わった時の処理 this._deleteTest(this.models.length - 1); // とりあえず最後に追加されたmodelを削除してみる }, // destroyのテスト用メソッド // @param {Number} deleteId 削除するmodelのid _deleteTest : function _deleteTest(deleteId) { var self = this; var model = _.first(this.where({ id: deleteId })); model.destroy({ success : function success(model, res) { self.trigger('onDelete', model); // イベント発火 } }); }, _onDelete : function _onDelete(model) { // 削除したmodelに対して何かするときはここに書く } }); var userCollection = new UserCollection({}); userCollection.fetch({ success : function success(collection, res, options) { collection.trigger('onFetch'); } });
↑のコードは最初にfetch()してcollectionにmodelが追加して、
最後に追加されたmodelを削除するという流れになってます。
destroy()が成功したら'onDelete'というイベントを発火させてますが、
単一のmodelが削除されたら何かするってことは実際はあまりなくて、
あるcollectionのmodelが削除されたらもう一度collectionをfetchし直して、
新しいリストを取得するというのが簡単です。
おわりに
今回はModelとCollectionの話だけですが、このModel/Collectionの
データの変化に対応してViewも変化させるのが必要です。
連携方法としては、collectionをfetchとかすると'sync'イベントが走るので、
それをView側で購読して、発火したらrender()するって流れになると思います。
次はBackbone.Routerとかやりたいですね。