Backbone.jsでPOSTでXMLを返す

2012.10.02

はじめに

Backbone.jsはデフォルトではRESTfulでJSONを返すWeb APIを使用する前提で書かれています。 ただ、全部POSTだけ又はGETだけを使用している既存のAPIを使いまわす場合や、政治的な理由でREST+JSONに出来ない、けれどもBackbone.jsを使いたい場合もあると思います。 そんな場合もBackbone.jsではBackbone.syncをオーバーライドすることで対応できるように作られています。

http://backbonejs.org/#Sync

ドキュメントにも「デフォルトではRESTful JSONだけど、俺をオーバーライドしたらWebSocket使ったりXML返したりLocal Strage使ったりできるZ」と書かれています。

まずは動くものを

http://backbone-post-sample.herokuapp.com/ シンプルで興味深いデータが表示されているはずです。これは画面表示時にPOSTリクエストしたデータがXMLで返り、パースしたものが表示されています。*GoogleChromeでのみ動作確認しています。

Backbone.syncをオーバーライドする

実際にCRAD全てPOSTでXMLを返すBackbone.syncサンプルを書いてみました。本家のBackbone.syncから少し変更しています。

(function(){
  // *1 CRAD全てのHTTPメソッドをPOSTにする
  var methodMap = {
    'create': 'POST',
    'update': 'POST',
    'delete': 'POST',
    'read':   'POST'
  };

  // Map from CRUD to HTTP for our default `Backbone.sync` implementation.
  // var methodMap = {
  //   'create': 'POST',
  //   'update': 'PUT',
  //   'delete': 'DELETE',
  //   'read':   'GET'
  // };

  Backbone.sync = function(method, model, options) {
    var type = methodMap[method];

    // Default options, unless specified.
    options || (options = {});

    // Default JSON-request options.
    //var params = {type: type, dataType: 'json'};
    var params = {type: type, dataType: 'xml'}; // *2 jQuery.ajax(options)のdataTypeをXMLに変更する

    // Ensure that we have a URL.
    if (!options.url) {
      params.url = getValue(model, 'url') || urlError();
    }

    // Ensure that we have the appropriate request data.
    if (!options.data && model && (method == 'create' || method == 'update')) {
      params.contentType = 'application/x-www-form-urlencoded';
      params.data = JSON.stringify(model.toJSON());
    }

    // * 古いサーバー用の記述。今回は使わないのでコメントアウト
    // // For older servers, emulate JSON by encoding the request into an HTML-form.
    // if (Backbone.emulateJSON) {
    //   params.contentType = 'application/x-www-form-urlencoded';
    //   params.data = params.data ? {model: params.data} : {};
    // }

    // // For older servers, emulate HTTP by mimicking the HTTP method with `_method`
    // // And an `X-HTTP-Method-Override` header.
    // if (Backbone.emulateHTTP) {
    //   if (type === 'PUT' || type === 'DELETE') {
    //     if (Backbone.emulateJSON) params.data._method = type;
    //     params.type = 'POST';
    //     params.beforeSend = function(xhr) {
    //       xhr.setRequestHeader('X-HTTP-Method-Override', type);
    //     };
    //   }
    // }

    // Dont process data on a non-GET request.
    if (params.type !== 'GET' && !Backbone.emulateJSON) {
      params.processData = false;
    }

    // Make the request, allowing the user to override any Ajax options.
    return $.ajax(_.extend(params, options));
  };
  // Helper function to get a value from a Backbone object as a property
  // or as a function.
  var getValue = function(object, prop) {
    if (!(object && object[prop])) return null;
    return _.isFunction(object[prop]) ? object[prop]() : object[prop];
  };

  // Throw an error when a URL is needed, and none is supplied.
  var urlError = function() {
    throw new Error('A url property or function must be specified');
  };  
})();

*1methodMapオブジェクトで定義されているHTTPメソッドのマッピングを全てPOSTに変更します。これで Modelのfetch, save, destroyのどのメソッドを呼んでもPOSTになります。

*2 jQuery.ajax(options)のdataTypeをXMLに変更し、戻りがXML形式ということを設定します。

動作確認用のサンプルコード

実際にread(読み取り=表示)でPOSTしてXMLで返されたデータを表示するサンプルです。

html

<!DOCTYPE html>
<html>
<head>
  <meta charset="UTF-8">
  <title>Backbone post sample</title>
</head>
<body>
  
  <ol id="users"></ol>

  <script src="//cdnjs.cloudflare.com/ajax/libs/jquery/1.8.2/jquery.min.js"></script>
  <script src="//cdnjs.cloudflare.com/ajax/libs/underscore.js/1.3.3/underscore-min.js"></script>
  <script src="//cdnjs.cloudflare.com/ajax/libs/backbone.js/0.9.2/backbone-min.js"></script>
  <script src="javascripts/post.js"></script>
  <script type="text/template" id="list-template"_>
      <li>
        <%= user.name %> / <%= user.team %>
      </li>
    </ul>
  </script>
</body>
</html>

JavaScript

$(function(){

  var User = Backbone.Model.extend({

    defaults:{
      name: '',
      team: '',
    },

  });

  var UserCollection = Backbone.Collection.extend({
    model: User,

    initialize: function(data) {
      this.url = 'getuser';
    },

    parse: function(res) {
      var parsed = [];
      var $users = $(res).find('users');  

      $users.find('user').each(function(j) {
        parsed.push(new User({
          name: $(this).find('name').text(),
          team: $(this).find('team').text(),
        }));
      });
      return parsed;
    }
  });

  var UserListView = Backbone.View.extend({

    el: $('#users'),

    template: _.template($('#list-template').html()),

    initialize: function() {
      this.options.collection.bind('reset', this.render, this);
    },

    render: function(){
      var list = this.collection.toJSON();
      _.each(list, function(user){
        this.$el.append(this.template({user:user}));
      }, this);
      return this;
    },
  });

  var AppRouter = Backbone.Router.extend({
    routes: {
        '': 'list',
    },

    list:function () {
      this.userList = new UserCollection();
      this.userListView = new UserListView({collection:this.userList});
      this.userList.fetch();
    },
  });
  router = new AppRouter();
  Backbone.history.start();
});

まとめ

Backbone.sync周りを変更することで、大抵のCRADの方法に適応できそうです。サンプルのtodoアプリではLocalStrageにデータを保存しているので、それも参考になると思います。またBackbone.syncは通信やLocalStrageのアクセスなどで、Backboneフレームワークでは最後の砦になるので、通信周りなどで共通のルールがある場合にこの辺をごにょごにょすれば対応できると思います。 とは言っても、Backboneを使う前提であればサーバーサイドの設計は特に理由がなければRESTにした方が自然でしょう。

https://github.com/fukasawa-takeshi/backbone-post-sample サーバーサイドも含めたソースです。

おまけ

http://cdnjs.comについて 最近知ったCDNサービスです。jQuery等はGoogleのCDNにホスティングされていますが、他のライブラリでCDNを利用してレスポンス改善したい場合に使えそうです。ただ、肝心のレスポンスに時間がかかる事があったりするので、本番運用ではまだちょっと不安な気がします。サンプルを作るには手軽なのでちょうど良い感じです。