Backbone.js

あけましておめでとうございます、south37です。

さて、僕の最近のマイブームはJavaScriptなんですが、クライアントリッチなアプリケーションを作るのが世間の流行りらしいので、世間的にもJavaScriptはブームと言えるでしょう。そんなJavaScript界隈の中でも特に目立って使われているのが、表題でもあるBackbone.jsだと思います。今日は、そんなBackbone.jsについてまとめてみたいと思います。

Backbone.jsって?

さて、まずBackbone.jsって何なんでしょう。このページによれば、MVCの形でWebアプリケーションにアーキテクチャという背骨を提供してくれるフレームワークだそうです。
具体的には、Event, Model, Collection, View, RouterというBackboneが提供する枠組みにのっかる事で、構造化したJavaScriptコードを書く事が出来ます。あくまで枠組みを提供するだけである為、非常にサイズの小さなフレームワークとなっている事もポイントです。

Model編

では、実際にコードを見てみましょう。
まずは、Modelの生成をしてみます。ただ生成するだけなら、

var obj = new Backbone.Model();

のようにnew演算子を適用してやるだけです。これで、model用のプロパティやメソッドを持ったobjオブジェクトが作られました。ただ、実際にはBackbone.Modelコンストラクタを少し拡張してからオブジェクトを生成するのが普通です。例えば

  1 var Staff = Backbone.Model.extend({
  2   defaults: {
  3     "name": "",
  4     "age": 0,
  5     "updateTime": new Date()
  6   },
  7   initialize: function() {
  8     console.log("Staff[" + this.cid + "]: " + JSON.stringify(this));
  9   }
 10 });
 11
 12 var tmpStaff = new Staff();
 13 console.log(tmpStaff);

のようなコードを実行すると、ブラウザのConsoleでは

Staff[c1]: {"name":"","age":0,"updateTime":"2014-01-02T12:00:24.165Z"} 

のような出力がなされます。順に解説していくと、最初にModelのextendメソッドを呼び出す事で、Modelのプロパティやメソッドを継承したStaffコンストラクタを生成しています。このextendメソッドは中でunderscore.jsのextend*1を呼び出しており、Modelの持つプロパティを全て新しいオブジェクトにコピーする事で擬似的に継承の機能を実現しています*2

いい忘れていましたが、underscore.jsを内部で使い倒しているのもbackbone.jsの特徴です。その為、underscore.jsを読み込んでおかなければbackboneは使えません。

Staffの定義に戻ると、defaults、initializeプロパティを何やら書き換えています。defaultsは生成されるオブジェクトの初期値を決めるプロパティで、initializeはオブジェクト生成時に実行されるメソッドです。initializeの中ではオブジェクトの詳細をConsole.logで出力しています。thisが生成されたオブジェクト自身で、this.cidってのはオブジェクト生成時に振られる識別子(これを割り振るのもbackboneの機能)、またオブジェクト自身はJSON.stringifyでjson文字列に変換し、出力しています。

細かい事を言うと、defaultsやコンストラクタに渡した値でデフォルト値が設定された後、initializeは実行されるみたいです。つまり、オブジェクト生成時に

var tmpStaff = new Staff({"name": "south37", "age": 20});

という風にパラメータを渡してやると、initializeで実行された出力は

Staff[c1]: {"name":"south37","age":20,"updateTime":"2014-01-02T12:26:49.395Z"} 

って感じになります。

今のところmodelらしさは微塵も感じませんが、JavaScriptコンストラクタがいろいろ拡張されてて使い易くなってる気はしますね。

Collection編

次はCollectionを触ってみましょう。CollectionはModelのリストです。リストを操作する為の便利なメソッドを多数持っています。例えば、

  1 var obj1 = new Backbone.Model({"name": "Murata", "age": 20});
  2 var obj2 = new Backbone.Model({"name": "Kenichiro", "age": 30});
  3
  4 var objs = new Backbone.Collection([obj1, obj2]);
  5 console.log(JSON.stringify(objs.get("c1")));
  6 console.log(JSON.stringify(objs.at(0)));
出力
{"name":"Murata","age":20} 
{"name":"Murata","age":20} 

のようなコードではCollectionのgetメソッドやatメソッドが使われています。getメソッドではcidを、atメソッドではCollection内のModelの位置を指定してやる事で、対象となるModelを取り出す事が出来ます。また、Collectionはsortも出来ます。

objs.comparator = function(item) {
  return item.get("age");
};
objs.sort();

のようにcomparatorプロパティにソート順を比較する為の関数を指定してやる事で、sortが実行出来るようになります。尚、上記の例ではmodelのageプロパティの値を比較してソートを行っています(itemに一つ一つのmodelが入ります。また、modelのgetメソッドではmodelのプロパティが取得出来ます)。
Collectionのlengthプロパティで要素数が取得出来、removeやeachによるイテレーション、findによる探索等も出来ます。他にもunderscore.jsで提供されている様々な便利なメソッドを使う事が出来ます。詳しくは、参考にしたこのページを見てみてください。

データの永続化

ModelとCollectionのデータの永続化について考えてみたいと思います。実は、Backboneは標準で、ajaxを利用したJSONインターフェースによるサーバへのデータ永続化機能を持っています。簡単に言えば、用意されたメソッドを呼び出すだけで非同期でサーバにJSONデータを送りつけてくれます(DBへの保存はサーバで行います)。

とりあえず、再びコードを見てみましょう。

var Memo = Backbone.Model.extend({
    urlRoot: "/memos",
    defaults: {
        "content": ""
    }
});
var memo = new Memo();
memo.save({content: "first memo"}, {
  success: function() {
    console.log(JSON.stringify(memo));
});

こんな感じでmodelのsaveメソッドを実行すると、POSTでサーバに{content: "first memo"}というJSONを送りつけます。ちょうどrailsでsaveでDBへの保存を行うような事を、クライアントサイドからサーバの動きを抽象化しながら行えるのが面白いですね。
ちなみに、Memoの定義で指定してるurlRootは、アクセスするサーバのurlを指定しています。ここでは、/memosにアクセスする設定にしてあります。

ModelのメソッドとHTTPリクエストの種類を表にまとめてみると、

(HTTPメソッド) (URL) (処理) (対応するメソッド)
GET /memos 一覧取得 idが未設定のModelによるfetch(または Collectionによるfetch)
POST /memos 新規作成 idが未設定のModelによるsave(または Collectionによるcreate)
GET /memos/:id 参照 idが設定済みのModelによるfetch
PUT /memos/:id 更新 idが設定済みのModelによるsave
DELETE /memos/:id 削除 idが設定済みのModelによるdestroy

のような感じになります。railsのようなサーバーサイドフレームワークにおける処理とそこそこいい感じに対応していますね。

Ruby on RailsでBackbone.jsを試す

ハイ、ではここで一度railsでbackboneを試してみたいと思います。ちょうど最近メモアプリ作りたいなーって気持ちになってたんですよね。

って事で、まずはrailsコマンドでアプリを作成。

rails new SimpleMemo

次に、Gemfileに

gem 'rails-backbone'

を追記してから*3

bundle install
rails g backbone:install

railsでbackboneを使う準備はOKです。

memoを保存出来るようにしたいので、

rails generate scaffold Memo content:string
rake db:migrate
rails g backbone:scaffold Memo content:string

のようにscaffoldしまくります。これで、backbone用のファイルが作られまくります*4

さて、ここでapp/assets/javascripts/backbone/models/memo.js.coffeeを開いてみると、

  1 class SimpleMemo.Models.Memo extends Backbone.Model
  2   paramRoot: 'memo'
  3
  4   defaults:
  5     content: null
  6
  7 class SimpleMemo.Collections.MemosCollection extends Backbone.Collection
  8   model: SimpleMemo.Models.Memo
  9   url: '/memos'

のようなコードが生成されています。CoffeeScriptなのでclass演算子があったりextendsが演算子っぽくなっていたりしますが、やってる事はModelをextendで拡張したコンストラクの定義です*5
paramRootってのはbackbone-railsの独自拡張のようで、詳しい使い方はよく分かりません。

今回はModelだけ使うのでurlRootプロパティを書き足して*6saveを実行してみると、確かにサーバー側のDBにデータが保存されていました。

  1 class SimpleMemo.Models.Memo extends Backbone.Model
  2   paramRoot: 'memo'
  3   urlRoot: '/memos'
  4
  5   defaults:
  6     content: null
  7
  8 class SimpleMemo.Collections.MemosCollection extends Backbone.Collection
  9   model: SimpleMemo.Models.Memo
 10   url: '/memos'
 11
 12 memo = new SimpleMemo.Models.Memo content: "first memo"
 13 memo.save {}, success: ->
 14   console.log JSON.stringify(memo)
出力
{"content":"first memo","id":3,"created_at":"2014-01-02T15:14:46.829Z","updated_at":"2014-01-02T15:14:46.829Z"} 
sqlite> select * from memos;
        id = 1
   content = first memo
created_at = 2014-01-02 15:05:39.921291
updated_at = 2014-01-02 15:05:39.921291

大成功ですね!!

今日はここまでです。
次回以降で、Backboneのその他の機能であるeventやrouter、viewについて説明していきたいと思います。backboneとrailsの連携についても同時平行で書いていきたいと思います。

*1:このページ公式ホームページに載っている通り、あるオブジェクトの持つプロパティ全てを別のオブジェクトにコピーする関数です。

*2:ちなみに、underscore.jsのextendは本当にただオブジェクトのプロパティをコピーするだけなんですが、Backbone.jsのextendメソッドではそれに加えてちゃんとconstructorプロパティや__super__プロパティをセットしています。要は、ちゃんと継承になるように追加で処理を行っています。

*3:ここが実はハマりポイントで、backbone-railsという名前なのにGemfileにはrails-backboneと書きます。30分近くとられてしまいました。

*4:ここではmodelを試したいだけなのでscaffoldする理由は全く無いのですが、細かい事考えるのが面倒なのでscaffoldしちゃってます。

*5:ちなみに、Modelのextendメソッドを呼び出す訳では無く、CoffeeScriptによって生成される__extend関数を使う為、厳密にはCoffeeScriptを使わないパターンとは違う処理です。ただ、コードを見た感じだとどっちもやってる事はほとんど変わらないっぽいです。

*6:Collectionに含まれるModelは、urlRootを指定しなくてもCollectionにurlプロパティが指定してあればOKらしいですが、今回はModelをCollectionと関連づけずにsaveしている為urlRootを定義しています。