はじめに
Rails で Backbone.js を使ってアプリを開発するときは、決まって backbone-rails を使っていた。
…んだけど、Backbone.js の v1.0.0 が出たというのに、backbone-rails は未だ対応してない(2013/07/02 現在)。
JavaScript ライブラリを gem でインストールすることに違和感あったし、 この際なんで backbone-rails にサヨナラすることに決めた。
これから JavaScript ライブラリは Bower で管理していく。 以下は移行メモ。
Bower をインストール
あらかじめ Node を homebrew や nodebrew や nvm を使ってインストールしておき、
npm install -g bower
を実行して Bower をグローバルにインストールする。 -g オプションは必須。
Bower を使って JavaScript ライブラリをインストール
Rails プロジェクトのルート(RAILS_ROOT) に .bowerrc と components.json を作成する。
まず .bowerrc。
{ "directory": "vendor/assets/components", "json": "component.json" }
RAILS_ROOT/vendor/assets/components 下にインストールするように設定している。
次に components.json。
{ "dependencies": { "jquery": "latest", "jquery-ujs": "latest", "underscore": "latest", "backbone": "latest" } }
Backbone.js を Rails の csrf-token や params に対応させる、rails-backbone の backbone_rails_sync.js が Backbone.js 1.0.0 に対応してなかった。
フォークして修正するより、Backbone.sync のソースコードをもとに実装し直したほうが早そうだったんで、 同じようなものを自作してみた。
(function($) { var methodMap = { 'create': 'POST', 'update': 'PUT', 'patch': 'PATCH', 'delete': 'DELETE', 'read': 'GET' }; var getUrl = function(object) { if (!(object && object.url)) return null; return _.isFunction(object.url) ? object.url() : object.url; }; // 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'); }; // Override this function to change the manner in which Backbone persists // models to the server. You will be passed the type of request, and the // model in question. By default, makes a RESTful Ajax request // to the model's `url()`. Some possible customizations could be: // // * Use `setTimeout` to batch rapid-fire updates into a single request. // * Send up the models as XML instead of JSON. // * Persist models via WebSockets instead of Ajax. // // Turn on `Backbone.emulateHTTP` in order to send `PUT` and `DELETE` requests // as `POST`, with a `_method` parameter containing the true HTTP method, // as well as all requests with the body as `application/x-www-form-urlencoded` // instead of `application/json` with the model in a param named `model`. // Useful when interfacing with server-side languages like **PHP** that make // it difficult to read the body of `PUT` requests. Backbone.sync = function(method, model, options) { var type = methodMap[method]; // Default options, unless specified. _.defaults(options || (options = {}), { emulateHTTP: Backbone.emulateHTTP, emulateJSON: Backbone.emulateJSON }); // Default JSON-request options. var params = { type: type, dataType: 'json' }; // Ensure that we have a URL. if (!options.url) { params.url = getUrl(model) || urlError(); } // Ensure that we have the appropriate request data. if (options.data == null && model && (method === 'create' || method === 'update' || method === 'patch')) { params.contentType = 'application/json'; var data = {}; var attrs = options.attrs || model.toJSON(options); if (model.paramRoot) { data[model.paramRoot] = attrs; } else { data = attrs; } params.data = JSON.stringify(data); } // For older servers, emulate JSON by encoding the request into an HTML-form. if (options.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 (options.emulateHTTP && (type === 'PUT' || type === 'DELETE' || type === 'PATCH')) { params.type = 'POST'; if (options.emulateJSON) params.data._method = type; var beforeSend = options.beforeSend; options.beforeSend = function(xhr) { xhr.setRequestHeader('X-HTTP-Method-Override', type); if (beforeSend) return beforeSend.apply(this, arguments); }; } // Don't process data on a non-GET request. if (params.type !== 'GET' && !options.emulateJSON) { params.processData = false; } // If we're sending a `PATCH` request, and we're in an old Internet Explorer // that still has ActiveX enabled by default, override jQuery to use that // for XHR instead. Remove this line when jQuery supports `PATCH` on IE8. if (params.type === 'PATCH' && noXhrPatch) { params.xhr = function() { return new ActiveXObject("Microsoft.XMLHTTP"); }; } // Set Rails csrf-token. options.beforeSend = _.wrap(options.beforeSend, function(func, xhr) { if (!options.noCSRF) { var token = $('meta[name="csrf-token"]').attr('content'); if (token) { xhr.setRequestHeader('X-CSRF-Token', token); } } if (func) { return func(this, xhr); } }); // Make the request, allowing the user to override any Ajax options. var xhr = options.xhr = Backbone.ajax(_.extend(params, options)); model.trigger('request', model, xhr, options); return xhr; }; })(Backbone.$);
あとは
bower install
を実行すると、ライブラリが RAILS_ROOT/vendor/assets/components 下にインストールされる。
アセットパイプラインで読み込めるように設定する
RAILS_ROOT/config/application.rb を編集し、アセットのパスを追加する。
config.assets.paths << Rails.root.join("vendor", "assets", "components")
app/assets/javascripts/application.js を編集し、利用するライブラリを require する。
//= require jquery/jquery //= require jquery-ujs/src/rails //= require underscore/underscore //= require backbone/backbone //= require backbone_rails_sync/backbone_rails_sync //= require_directory . //= require ./backbone/main
RAILS_ROOT/app/assets/javascripts/backbone を読み込むのは、backbone-rails の慣習。 この下に Backbone.js を使ったコードを置いている。
これで Backbone.js 導入完了
あとは RAILS_ROOT/app/assets/javascripts/backbone ディレクトリの中に、Backbone.js のモデルやビューを実装していく。
まとめ
JavaScript ライブラリの管理を gem から Bower に移行した。 これでライブラリがバージョンアップしたとき素早く対応できる。 今後、新規に Rails アプリを作る場合も、JavaScript ライブラリは Bower で管理する予定。 こうなってくると、Grunt や Yeoman も導入したくなってきた。
シングルページアプリケーションでは Rails で Web API を実装し、 クライアント側は Backbone.js と Grunt や Bower や Yoeman 等を使って実装する、 って感じのクライアントとサーバーが疎結合な構成にしていきたい。