Backbone.js再入門はソースコードリーディングから【その６】

# Backbone.jsのソースコードを読んでみる

今回はBackbone.sync
Backbone.syncは$.ajaxのラッパーライブラリです。
Backbone.Model, Backbone.CollectionなどもBackbone.syncを使用してAPIサーバーとHTTP通信を行います。
要は、Modelのattributesの内容を元にリクエストの自動生成などを行うイメージです。
結局は、Model, Collectionなどのリソースを元にHTTPリクエストをすることになるため、RESTなAPIでないと相性が良くないです。
あと、今回はBackbone.Modelも少し見ます。(Backbone.syncを利用しているメソッドがあるので)


# 対象Version

Backbone.js 1.2.3

# Backbone.sync

```js

  // Backbone.syncはHTTPのリクエストを行うためのAPI
  // 基本的にBackbone.syncを直接触ることは少なく、Backbone.Model.saveやBackbone.Collection.fetchなどの中でBackbone.syncを実行するようになっている
  // method: HTTPのメソッド名に変換するための識別子。詳しくはmethodMapのオブジェクトを参照
  // model: Backbone.Modelのインスタンス
  // options: リクエストのbodyなど色々、最終的に$.ajax()の引数にも利用されます。要はここに{success: Function, error: Function}とか書く必要があります
  Backbone.sync = function(method, model, options) {

    // HTTPのメソッド名がtypeには入ることになる、GET, POSTなど
    var type = methodMap[method];

    // optionsをオーバーライドする
    // emulateHTTP, emulateJSONの役割に関しては他のところで説明しているので割愛する
    _.defaults(options || (options = {}), {
      emulateHTTP: Backbone.emulateHTTP,
      emulateJSON: Backbone.emulateJSON
    });

    // 変数paramsは最終的に$.ajax()の引数に使われます
    var params = {type: type, dataType: 'json'};

    // options.urlが実装されていない場合はModel.urlを評価した結果を利用します。
    // いづれかのurlを取得できない場合(実装されていない場合は)例外を発行します
    // params.urlはHTTPのリクエストのエンドポイントです。
    if (!options.url) {
      params.url = _.result(model, 'url') || urlError();
    }

    // params.data(HTTPのBODYにあたるところ)を決定するためのアルゴリズムです
    // 
    // 更新系のリクエストの場合
    // options.attrsが実装されていればそれを利用します
    // options.attrsが実装されていない場合はmodel.toJSON()の結果を利用します
    if (options.data == null && model && (method === 'create' || method === 'update' || method === 'patch')) {
      params.contentType = 'application/json';
      params.data = JSON.stringify(options.attrs || model.toJSON(options));
    }

    // For older servers, emulate JSON by encoding the request into an HTML-form.
    // ここに関してもBackbone.emulateJSONで説明したので割愛
    if (options.emulateJSON) {
      params.contentType = 'application/x-www-form-urlencoded';
      params.data = params.data ? {model: params.data} : {};
    }

    if (options.emulateHTTP && (type === 'PUT' || type === 'DELETE' || type === 'PATCH')) {

    　// ここに関してもBackbone.emulateJSONで説明したので割愛
      params.type = 'POST';
      if (options.emulateJSON) params.data._method = type;
     
      // beforeSendはajaxで通信を行う直前に評価するメソッドです。
      // 通信の直前でヘッダーの書き換えを行います。
      // 仮にoptionsに対してbeforeSendが実装されていると、単純に上書きされてしまい、実装していたはずのbeforeSendが評価されなくなってしまうので
      // 一旦、実装されているbeforeSendをキャッシュして多段的に実行するようになっています。
      var beforeSend = options.beforeSend;
      options.beforeSend = function(xhr) {
        xhr.setRequestHeader('X-HTTP-Method-Override', type);
        if (beforeSend) return beforeSend.apply(this, arguments);
      };
    }

    // dataに指定したオブジェクトをクエリ文字列に変換するかどうかを設定します。
    // ajaxの機能です。GETリクエスト以外は不要です
    if (params.type !== 'GET' && !options.emulateJSON) {
      params.processData = false;
    }

    // エラーの際のハンドリング用のメソッドも少しカスタマイズして多段的に実行するようになっています
    // 最終的にoptionsを引数に受けたModelの『request』イベントが発行されるのでその時にstatusを簡単に補足するためにoptionsのメンバを更新していますね
    // Modelのイベント走らせるとかもいいと思うんですけどね。
    var error = options.error;
    options.error = function(xhr, textStatus, errorThrown) {
      options.textStatus = textStatus;
      options.errorThrown = errorThrown;
      if (error) error.call(options.context, xhr, textStatus, errorThrown);
    };

    // 実際に$.ajaxを実行してHTTPリクエストを飛ばしています。
    // リクエストを飛ばした際にModelの『request』イベントを発行しています。
    // リクエストが終了された時に発行されるイベントはありません。あったらloading画面の表示・非表示とか書きやすくなるんじゃないかなと思います。
    var xhr = options.xhr = Backbone.ajax(_.extend(params, options));
    model.trigger('request', model, xhr, options);
    return xhr;
  };

  // CURDベースの名前の付け方のものをHTTPのメソッド名に変換させます
  var methodMap = {
    'create': 'POST',
    'update': 'PUT',
    'patch':  'PATCH',
    'delete': 'DELETE',
    'read':   'GET'
  };

  Backbone.ajax = function() {
    return Backbone.$.ajax.apply(Backbone.$, arguments);
  };

```

- ModelのイベントrequestはHTTPのリクエストが完了した際のイベントではなく、HTTPのリクエストを飛ばした直後に発行されるイベントである。
- method(CURD)の種類やemulateaによってリクエストの内容の生成がごちゃごちゃ変わるので追いづらくたいへんです。
- HTTPリクエストのURLはメソッドの種類にかかわらず、Model.urlの評価結果を元に決定するようになっているので、RESTではないAPIとの相性が悪そう
- おそらくBackbone.syncを生で使うことはBackbone.jsでは期待されておらず、(Model/Collection).(save/fetch/destroy)などでラップしたほうがいいです。




# Backbone.Model

```js

    fetch: function(options) {
      options = _.extend({parse: true}, options);
      var model = this;

      // ajaxのsuccessのcallbackを上書きする
      // 基本的にはレスポンスの内容(key/value)をmodelのattributesとして.setで登録することが役割である。
      // また、successが評価された場合に『sync』イベントを発行する
      var success = options.success;      
      options.success = function(resp) {
        var serverAttrs = options.parse ? model.parse(resp, options) : resp;
        if (!model.set(serverAttrs, options)) return false;

        // 元々定義していたsuccessを評価する
        if (success) success.call(options.context, model, resp, options);
        model.trigger('sync', model, resp, options);
      };
      
      // wrapErrorは『error』イベントを発行するためのユーティリティ
      wrapError(this, options);
      return this.sync('read', this, options);
    },

    // Backbone.syncを使って更新・登録処理をおこなう
    // options.validate: validationを実行するかどうか
    // options.parse: Model.parseにてattributesを加工するかどうか
    // options.wait: HTTPリクエストをする前にModelのattributesを書き換えるか
    save: function(key, val, options) {

      // 引数を→のように変換する{key, value}, options
      var attrs;
      if (key == null || typeof key === 'object') {
        attrs = key;
        options = val;
      } else {
        (attrs = {})[key] = val;
      }

      // saveの際もModel.setを利用するが、デフォルトではvaludate, parseを有効にする
      options = _.extend({validate: true, parse: true}, options);
      var wait = options.wait;

      // waitオプションが付いていない場合は先にthis.setを実行してModelのattributesを加工する
      // waitオプションが付いている場合はajaxのsuccessのコールバックの後にModelのattributesを加工することになります
      if (attrs && !wait) {
        if (!this.set(attrs, options)) return false;
      } else {
        if (!this._validate(attrs, options)) return false;
      }

      var model = this;
      var success = options.success;
      var attributes = this.attributes;

      // ajaxのsuccessのコールバックを定義しなおしている
      // Model.setの実行や、syncイベントの発行をするため
      options.success = function(resp) {

        // HTTPリクエストの結果をparseしたものとModelのattributesの内容をマージさせてModelの状態を更新します
        model.attributes = attributes;
        var serverAttrs = options.parse ? model.parse(resp, options) : resp;
        if (wait) serverAttrs = _.extend({}, attrs, serverAttrs);
        if (serverAttrs && !model.set(serverAttrs, options)) return false;
        if (success) success.call(options.context, model, resp, options);

        // model.setが完了してからsyncイベントを発行する
        model.trigger('sync', model, resp, options);
      };
      wrapError(this, options);

      if (attrs && wait) this.attributes = _.extend({}, attributes, attrs);

      // isNewでmodelにidが振られているかを確認する
      // idが存在する場合はupdateとみなします
      var method = this.isNew() ? 'create' : (options.patch ? 'patch' : 'update');
      if (method === 'patch' && !options.attrs) options.attrs = attrs;
      var xhr = this.sync(method, this, options);

      // Restore attributes.
      this.attributes = attributes;

      return xhr;
    },

    // Backbone.syncを使ってModelの削除を行います。
    // Modelが購読しているイベントも削除します
    // options.waitがある場合はHTTPリクエストの完了前にModelの削除を行います
    destroy: function(options) {
      options = options ? _.clone(options) : {};
      var model = this;
      var success = options.success;
      var wait = options.wait;

      var destroy = function() {
        model.stopListening();
        model.trigger('destroy', model, model.collection, options);
      };

      options.success = function(resp) {
        if (wait) destroy();
        if (success) success.call(options.context, model, resp, options);
        if (!model.isNew()) model.trigger('sync', model, resp, options);
      };

      var xhr = false;
      if (this.isNew()) {
        _.defer(options.success);
      } else {
        wrapError(this, options);
        xhr = this.sync('delete', this, options);
      }
      if (!wait) destroy();
      return xhr;
    },


```

- いずれもHTTPのリクエストが完了した際にsyncイベントを発行します
- wrapError関数はerrorイベントを発行します。HTTPリクエストが失敗した場合はerrorイベントを発行します
- saveの際に実行される.setはvalidate, parseオプションがデフォルトで有効になっています
- save時のwaitオプションはHTTPリクエストが完了した際に.setを行うか、HTTPリクエスト開始前に.setを行うかを制御する

# 終わりに

さて、次はBackbone.Collectionを読んでいきます。これが最後になりそうです。