yutaponのブログ

javascript界隈の興味あるネタを備忘録的に残しておく場所

Backbone.jsに入門してみる【サーバー通信編 (Model, Collection)】

今回はBackbone.ModelとBackbone.Collectionを使って
サーバーサイドとRESTな通信をしてみます。

サーバーとの通信ですが、別に難しいことはありません。
jQueryでいう $.get(), $.post() をBackbone.js風に使うだけです。

はじめに

サーバーとの通信ですが、今回は/usersというURLに対して行うことにします。
サーバー側はクライアント側からのRESTメソッドに対応した実装がされている前提です。

/usersというURLは名前からuserの集合を表していることがわかるかと思いますが、
このuserの集合というリソースに対してクライアント側から
任意のリソースを要求したり、リソースを作成したりできるのがRESTなAPIです。

この/usersに対してそれぞれのメソッドでリクエストした場合、
以下のような意味になります。
サーバー側ではそれぞれの意味になるように実装する必要があります。

  1. [GET] /users
    • userの集合を取得する
  2. [POST] /users
    • userを新規作成する
  3. [GET] /users/1
    • user_id: 1のuserを取得する
  4. [PUT] /users/1
    • user_id: 1のuserの情報を更新する
  5. [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とかやりたいですね。