Backbone.jsのSyncで起こっていること(Model編)


Backbone.jsで一度は使ってみたいお便利機能Sync。クライアントサイドのモデル・コレクションをサーバーと簡単に同期させることができるそうですが、リファレンス読んだだけでは実際にサーバーとのやりとりがどうなっているのか分からなかったので、自分なりに調べてみました。

Backbone.jsのModelではsave(), fetch(), destroy()、Collectionではfetch()の関数を持ちますが、これらを実行するとBackbone内部のsync()を通してサーバーとするようです。syncの内部で行われている実態は、jQueryなどのAjaxを呼び出しています。

BackboneはREST(CRUDという意味で使っています)スタイルで通信を行うので、Modelの状況を見つつsave(), fetch(), destroy()をcreate/read/update/deleteに割り当てているようです。詳しくは一度リファレンスを読んだ方が良いと思います。

とりあえず、Modelについて分かったことです。(Collectionは後日)

 




・modelがsyncを行う上で通信先のurlの情報が必要。modelがCollectionに属していない場合はurlRootの属性に'/foo'などのURLを指定する。実際に通信されるアドレスはurlRoot + model.idになる。

・model.save():id属性が無い場合は新規(isNew == true)とみなしcreate(POST)で通信を行う。id属性がある場合はupdate(PUT)で通信する。

・model.fetch():read(GET)で通信を行うが、どのデータを読みこめばよいか把握するためmodelにはid属性が必須になる(無い場合は通信しない)

・model.destroy():delete(DELETE)で通信する。fetch()と同じくid属性が必要。

で、実際にサーバーにデータが送られるわけですが、気をつけたいのはサーバーからのレスポンス。デフォルトではBackboneはJSON形式でデータを返されることを期待していますが、その返されたJSONの内容でModelを更新してしまいます。
通信成功したことのメッセージなど適当な情報いれておくと大変なことになってしまいますね。

通信の内容がわかるよう簡単にテストしてみました。
ちなみにサーバーはnode.js + expressで構築しています。


サーバー:app.js

var express = require('express')
, routes = require('./routes')
, http = require('http')
, path = require('path');

var app = express();

app.configure(function(){
app.set('port', process.env.PORT || 3000);
app.set('views', __dirname + '/views');
app.set('view engine', 'jade');
app.use(express.favicon());
app.use(express.logger('dev'));
app.use(express.bodyParser());
app.use(express.methodOverride());
app.use(app.router);
app.use(express.static(path.join(__dirname, 'public')));
});

app.configure('development', function(){
app.use(express.errorHandler());
});

app.get('/', routes.index);

// ----------ここからテストコード---------
// ID割り振り用の変数。
var id = 0;

// POST(create)
app.post('/foo', function(req, res){
console.log('----------post---------');
console.log('query: ',req.query);
// -> {}
console.log('params: ',req.params);
// -> []
console.log('body: ',req.body);
// -> { hoge: 'hoge' }
// DBなんかを使ったりして、ごにょごにょやる。

// Backbone.jsの既定の仕様では、
// モデルをサーバーからのレスポンスの内容で上書きする。
// ここで注意すべきはidを返していること。
// レスポンスにはステータスコードを付加できる。
res.status(201).send({"id": id});

// 擬似的にIDを割り振るたけのテストコード
id++;
});

// PUT(update)
app.put('/foo/:id', function(req, res){
console.log('----------put---------');
console.log('query: ',req.query);
// -> {}
console.log('params: ',req.params);
// -> [ id: '1' ]
console.log('body: ',req.body);
// -> { hoge: 'hoge', id: 1, foo: 'foo' }
// DBなんかを使ったりして、ごにょごにょやる。

// Backbone(もしくはJqueryajax)のバグだと思いますが、
// 何かしら返さないとsuccess関数が発火されません。
// 適当なメッセージを返すとモデルが上書きされるので注意!
res.status(200).send({});
});

// GET(read)
app.get('/foo/:id', function(req, res){
console.log('----------get---------');
console.log('query: ',req.query);
// -> {}
console.log('params: ',req.params);
// -> [ id: '1' ]
console.log('body: ',req.body);
// -> {}
// DBなんかを使ったりして、ごにょごにょやる。

res.status(200).send({"bar": "bar"});
});

// Delete(delete)
app.delete('/foo/:id', function(req, res){
console.log('----------delete---------');
console.log('query: ',req.query);
// -> {}
console.log('params: ',req.params);
// -> [ id: '1' ]
console.log('body: ',req.body);
// -> {}
// DBなんかを使ったりして、ごにょごにょやる。

res.status(200).send({});
});
// ----------ここまで---------

http.createServer(app).listen(app.get('port'), function(){
console.log("Express server listening on port " + app.get('port'));
});


サーバー:view/index.jade
単にどのライブラリを読み込んでいるかだけの説明のため。

extends layout

block content
h1= title
p Welcome to #{title}

script(src='/javascripts/jquery-1.9.1.min.js')
script(src='/javascripts/underscore-min.js')
script(src='/javascripts/backbone-min.js')
script(src='/javascripts/async.js')
script(src='/javascripts/client.js')


クライアント:client.js
非同期通信を順番に行うため「async.js」を利用しています。

(function() {

// Backbone.sync Model単体テスト
// ModelがCollectionに属さない場合はurlRootを指定する必要がある。
var TestModel = Backbone.Model.extend({
urlRoot: '/foo',
defaults:{
hoge: 'hoge'
}
});
var testModel = new TestModel();


// Backboneのsyncは非同期処理だけれども順番に行いたいので
// async.jsを活用する。
async.series([
function(callback){
// POST(create)
// Modelの属性にidが無いときは、サーバーに保存されていない
// (model.isNew() == true)とみなされ、
// POSTメソッドで送信される。
testModel.save(null,{
// serverからのresponseはsuccess関数で受け取る。
// エラーの場合はerror関数が発火する。
success: function(model, response, options){
console.log('success: ', model.toJSON());
// -> {hoge: "hoge", id: 0}

// asyncに定義した次の関数を呼び出す。
callback(null);
},
error: function(model, xhr, options){
// エラーの場合ここに処理を書く。
}
});
},

function(callback){
// PUT(update)
// Client側ではidを設定していないので
// POSTメソッドかと思いきや、先の通信でidが返され、
// modelに組み込まれているのでPUTメソッドになる。
testModel.save({foo: 'foo'},{
success: function(model, response, options){
console.log('success: ', model.toJSON());
// -> {hoge: "hoge", id: 0, foo: "foo"}

callback(null);
}
});
},

function(callback){
// GET(read)
// 通信先のURLは「urlRoot + model.id」になる。
testModel.fetch({
success: function(model, response, options){
console.log('success: ', model.toJSON());
// -> {hoge: "hoge", id: 0,
// foo: "foo", bar: "bar"}

callback(null);
}
});
},

function(callback){
// DELETE(delete)
// 通信先のURLは「urlRoot + model.id」になる。
testModel.destroy({
success: function(model, response, options){
console.log('success: ', model.toJSON());
// -> {hoge: "hoge", id: 0,
// foo: "foo", bar: "bar"}

callback(null);
}
});
},

function(callback){
console.log(testModel);
// destroyしても参照は残っていますねー。
// 明示的にdelete呼ばないといけないのでしょうか?
callback(null);
},
],

// asyncの最後に実行される関数。
function(err, results){
if(err) console.log(err);
else console.log('Completed!');
});

})();


うーん。destroy()したときに実際にmodelの破棄はどう行えばよいのかわかりませんでした。単純にdeleteすれば良いのかな?
にしても、ブログ上のコード表示見難いっすね・・・改善せねば。