注目の記事

Backbone.js -JavaScriptのMVCフレームワーク-

この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。

Backbone.jsとは?

Backbone.jsは、JavaScriptによる大規模なアプリケーション開発を行う際に力を発揮するMVCフレームワークです。データバインディングとカスタムイベントを備えたModel、配列情報を表すCollection、イベントをハンドリングするView、サーバーサイドのアプリケーションと連携するためのRESTful JSONなどをフレームワークとして備えています。

大規模な業務アプリケーションのユーザーインタフェースをJavaScriptでゴリゴリと作ろうとした場合、100%に近い確率で失敗するかと思います。これは、Flexのようなビルド時のコンパイラエラーを検出できないこと、存在するフレームワークがインタラクションやビジュアルに特化していること、ブラウザーやOSの組合せでAPIレベルの仕様が異なる事、同じブラウザーでもバージョンの違いにより挙動が異なる事、JavaScriptの記述が属人的になってしまう事などが挙げられます。約十数年前、私は数十万行のJavaScriptを記述するプロジェクトにアサインされて炎上した記憶がよみがえりますw。

時は流れて2011年師走、ブラウザーの開発ツールやブラグインでデバックやステップ実行が出来るようになり、ブラウザー間の互換性が重要視されるようになり、DOMやCSS操作を簡単にするjQueryが広まり、サーバーサイドとの連携にJSONがデファクトとなりました。まだまだFlexフレームワークの生産性には遠く及びませんが、そろそろJavaScriptで大規模業務アプリもいいんじゃない?ってことで始めて見たいと思います。なお、ここで言う業務アプリとは、フォーム入力系・一覧表示系などの画面が数百画面あるようなアプリケーションを想定しています。グイグイ動きゲーム性のあるネイティブアプリは想定していません。

データバインディングとカスタムイベント

モデルの内容か状態が変化するとき、そのモデルを登録した他のオブジェクトにイベントが通知されます。ここで、ビューは手動で見た目を変更するのではなく、モデルの変更を検出して自動的に見た目を更新します。

可算可能なリッチなコレクションAPI

Backbone.jsは、データを処理して作業するための非常に多くのリッチなAPIを持っています。JavaScriptは、他の言語実装とは異なり、配列はかなり中途半端で、データを処理しなければならない場合に手続きの妨げになる問題があります。

ビューと宣言的なイベントのハンドリング

スパゲッティな呼び出しを書く日々は終わりです。どのコールバックが指定された要素に関係している必要があるか、プログラムによって宣言することができます。

RESTful JSONインタフェース

デフォルトの関数として、サーバーサイドのアプリケーションとAjax通信をするものが用意されています。また、必要に応じてこのアダプター部分を切り替える事が出来ます。WebSocketやローカルストレージとの通信など、あったらいいなが始めから用意されています。

jQueryの代わりになるもの?

いいえ。Backbone.jsは高レベルで抽象化された仕組みを提供するフレームワークです。

なぜBackbone.jsを使うベキなのか?

通常は、フロント側のコードは、入れ子のコールバックになったり、DOM操作をしたり、HTMLを汚くしてしまいます。Backbone.jsは、これらの混乱を避けて秩序を取り戻す、清潔で上品な方法を提供します。たぶん。

どこでBackbone.jsを使うべきなのか?

フロント側のアプリケーションが大規模やファットな場合です。例として、GmailやTwitter、Facebookなどが考えられます。

CappuccinoやSproutcoreと似ている?

はい/いいえ。どちらのフレームワークも複雑なWebアプリケーションを作るという点では「はい」です。違いとしては、Backbone.jsはとても軽量である点が挙げられます。CappuccinoはObjective-Jの記述が強制されますし、SproutcoreはビューをJSで書く必要があります。これらの違いはどちらかが間違っているというものではなく、Backbone.jsはより良い学習曲線を描く事ができるとしておきましょう。(あんまり他の悪口は言いたくない。)

他のライブラリと併せて使う事ができますか?

もちろんです。典型的なDOMアクセスや、Ajax通信をラッピングするだけではなく、残りのテンプレートやスクリプトを読み込むことにも使います。Backbone.jsは、おそらく他のあらゆるツール類と疎結合することができます。

Backbone.jsのバックボーン

Backbone.jsのMVCは、当初フレームワークにControllerが無かったため、Model、View、Collectionで表現していました。今は既に変わっています。Bockbone.jsの主要なクラスは以下です。

  • Model
  • Collection
  • View
  • Controller

Modelについて

Modelは、MVCの実装による違いによって異なる意味を持つ事ができます。Backbone.jsでは、ひとつのエンティティを表します。まぁ、データベースの1レコードに相当するものとでも思ってください。ただし、これは強制的なものではありません。Backbone.jsのウェブサイトには以下のように記述されています。

Modelは、JavaScriptアプリケーションの心臓となるもので、インタラクティブなデータと同じようにそれらを囲う大部分のロジックも含んでいます。(比較、検証、プロパティ演算、アクセスコントロール)

Modelは、単にデータセット上で任意の性質や属性を読み書きする方法を与えます。

var Game = Backbone.Model.extend({});

これにちょっと追記してみます。

var Game = Backbone.Model.extend({
    initialize: function(){  
        alert("Oh hey! ");  
    },  
    defaults: {  
        name: 'Default title',  
        releaseDate: 2011,  
    }  
});

initializeは、オブジェクトが初期化されたら呼び出されます。ここでは単にアラート表示しているだけです。実際のアプリケーションでは、この部分に一連の処理を記述して必要なデータを取ってくるなどをするわけですね。defaults は、データが指定されなかった場合の処理を記述します。それでは、実際に読み書きを行ってみましょう。はじめにインスタンスを生成することを忘れずに。

//インスタンス生成
var portal = new Game({ name: "Portal 2", releaseDate: 2011});  
  
//releaseDate属性の取得
var release = portal.get('releaseDate');  
  
//name属性の変更
portal.set({ name: "Portal 2 by Valve"});

Modelの属性に対して、オブジェクト名.属性名の形式では読む事はできません。これは、ユーザーの間違いによってデータを変更してしまう可能性を下げるために、getter/setterの形式でアクセスする必要があります。また、ポイントして、全てのデータの変更はメモリー上のみで行われています。サーバーに送って変更を永続的にしましょう。

portal.save();

たったこれだけです。1行書くだけでサーバーにリクエストが飛びます。なお、リクエストタイプは知的に変更されることを覚えておいてください。今回は新しいオブジェクトでしたのでPOST通信が使われます。そうでなければ、PUTが使われます。他にも多くの機能がありますが、手始めに基本的な機能をご紹介しました。他にももっと知りたい方は公式ドキュメントを見てください。

Collectionについて

Backbone.jsにおけるCollectionは、基本的にModelの配列を表します。以前、データベースの比喩を使いましたが、CollectionはSelectクエリー結果を表しています。複数のレコードが返され、それぞれのレコードが1Modelを表しています。以下のようにCollectionを定義できます。

var GamesCollection = Backbone.Collection.extend({  
  model : Game,  
  }  
});

まずはじめに、どのModelの配列にするのか宣言します。以前の例に続けて、Collectionとして、Game(Model)の配列を作成します。これで、コンテンツに対して好きに操作することができるようになります。例えば、Collectionを拡張して該当するゲームを返す関数を追加します。

var GamesCollection = Backbone.Collection.extend({  
  model : Game,  
  old : function() {  
    return this.filter(function(game) {  
      return game.get('releaseDate') < 2009;  
    });  
  }  
  }  
});
[/javascript]

<p>とても簡単ですね。単に2009年よりも前のゲームを返しているだけです。以下のように直接コンテンツを操作することもできます。</p>


var games = new GamesCollection  
games.get(0);

Collectionを生成してから内部のModelを取り出すためにID 0と指定しています。これは、先頭からの要素の位置を指定して取得しています。最後に、Collectionに対して直接的に操作する方法についてご紹介します。

var GamesCollection = Backbone.Collection.extend({  
  model : Game,  
  url: '/games'  
  }  
});  
  
var games = new GamesCollection  
games.fetch();

これはBackbone.jsが、URLプロパティを通じてデータを取得する方法です。fetch関数を呼び出す事によって、非同期にサーバーを呼び出して結果の配列を取得し新しいオブジェクトに入れます。

Backbone.jsでのCollectionの基本的な扱いについてご紹介しました。Underscore.jsライブラリの多くの素晴らしいユーティリティとBackbone.jsを組み合わせることで、技の数々を得ることができます。詳しくは公式ドキュメントを参照してください。

Viewについて

Backbone.jsにおけるViewは、最初に少々ややこしい説明をしました。MVC信者たちはViewよりもControllerに似ていると思うはずです。Viewは、2つの基本的な役割を持っています。

  • DOMとModel/Collectionから投げられるイベントを待つ
  • アプリケーションの状態とデータモデルをユーザに届けるための表現

とても簡単なViewを作ってみます。

GameView= Backbone.View.extend({  
  tagName : "div",  
  className: "game",  
  render : function() {  
    // ViewのためのHTMLを描画するためのコード  
  }  
});

HTML要素は、className属性を使ったIDの指定と同様にtagName属性を使ったViewのラップに使われるべきとチュートリアルには示されています。実際に見てみましょう。

render : function() {  
  this.el.innerHTML = this.model.get('name');  
  
  //まはたjQueryの記法
  $(this.el).html(this.model.get('name'));  
}

elはViewから参照されるDOM要素によって参照されます。今回はゲームのnameについて要素のinnerHTMLプロパティを用いてアクセスしています。反映するのは簡単で、div要素にゲーム名を設定すればよいです。jQueryの記述によって明瞭に記述する事もできます。複雑なレイアウトの場合にJavaScript内でHTMLを記述するのは無謀です。このシナリオの場合には、テンプレート機能を使う事をお勧めします。Backbone.jsはUnderscore.jsの作法を用いた小さなテンプレート機能を用いることができます。もちろん、これ以外の素晴らしいテンプレートソリューションを使う事もできます。最後に、Viewがどのようにイベントを待っているか見てみましょう。DOMイベントが最初です。

events: {  
        'click .name': 'handleClick'  
    },  
    handleClick: function(){  
        alert('In the name of science... you monster');  
  
        // 必要あれば他の処理  
}

以前にイベントを使った事があるならとても簡単ですね。基本的にはイベントオブジェクトを通じてイベントを定義して繋いでいます。最初の部分ではイベントを指定しています。次の部分ではトリガーとなる要素を指定しています。最後の部分では実行される関数を指定しています。次に、ModelとCollectionに対するデータバインディングについてご紹介します。

GameView= Backbone.View.extend({  
initialize: function (args) {  
        _.bindAll(this, 'changeName');  
          this.model.bind('change:name', this.changeName);  
},  
});

はじめに、初期化関数の中でデータバインディングに関する記述を行います。この部分に書くのが自然でものごとを進めやすいです。bindAll関数は関数のthis値を接続するUnderscore.jsのユーティリティです。コールバックでthis値が消去されているような指定された周囲の関数と関数の束を渡していてこれは特に便利です。関数がコールバックで呼び出される場合、関数内のthisの指す先が変わってしまうので、接続しておかないと意図したプロパティを処理してくれません。今、Modelのname属性が変更されたとして、changeName関数が呼び出されます。また、addやremoveを使って変更を監視することができます。Collection内で変更を聞いているのは、Collection内のModel置き換えに対して、コールバックにハンドラーを結びつけるのと同じぐらい簡単です。

Controllerについて

Backbone.jsにおけるControllerは、基本的にシバン記号を用いてアプリケーションの状態保持やブックマーク作成を提供します。

var Hashbangs = Backbone.Controller.extend({  
  routes: {  
    "!/":                 "root",  
    "!/games":        "games",  
  },  
  root: function() {  
    // ホームページの記述
  },  
  
  games: function() {  
    // 書籍の配列に関するビューを再描画する 
  },  
  });

このやり方は、古典的なサーバーサイドのMVCフレームワークと似ています。例えば、!/gamesと指定すれば、URLでdomain/#!/gamesが呼び出されるとか。このシバン記号を使うことで重いJavaScriptアプリであってもブックマーク可能になるわけです。もし、戻るボタンを心配しているならば大丈夫、Backbone.jsはこれについても考慮されています。

var ApplicationController = new Controller;   
  
Backbone.history.start();

上記の記述のように、Backbone.jsはシバンを監視していて、指定したルートと共にアプリをブックマーク可能です。

Backbone.jsのまとめ

  • フロントエンドにMVCは必要。昔ながらのやり方はものごとを複雑にして、維持が難しく、結合されたコードを私たちに残します。
  • DOMにデータや状態を保持するのは悪い考えです。理にかなった始め方は、アプリ作成後に同じデータで更新されるアプリの異なる部分を必要に応じて作成することです。
  • ファットなモデルとスキニーなコントローラが適しています。ビジネスロジックはモデルによって行われるとワークフローは簡素化されます。
  • テンプレートは必要不可欠です。JavaScript内にHTMLを置くことは、あなたに罰が当たるでしょう。

まとめ

フロントエンドにMVCを持ってくる考えは、まさにFlexと同じ思想。とってもすんなり理解できました。実際の開発では、Underscore.jsなどと組み合わせて、いかに短いコードでやりたいことを実現するかがカギになってきそうです。サーバーサイドのMVCフレームワークに慣れている人や、Flexを使った開発に慣れている人は、ぜひBackbone.jsの採用を検討されてはいかがでしょうか。動的に生成されるデータはサーバーサイドで生成されるJSONデータのみにして、フロントエンドでは静的ファイルを基にした描画を行い、静的ファイルはAmazon S3などに置いてサーバーサイドの負荷を軽減し、データモデルの変更やユーザー操作イベントによってフロントエンドの見た目が変化する作り方が最近のお気に入りです。Backbone.jsをマスターしてフロントMVCをマスターしよう!

参考資料

Getting Started with Backbone.js