wataメモ

日々のメモをつらつらと書くだけ

Backbone.jsを使ってみた

 JavaScriptでゴリゴリ書く時にMVCライクに色々整理できたらいいなぁと思うことがある。そんな中、巷で有名になりつつあるBackbone.jsを使ってみた。いつもの「使ってみた」の流れでライトに使って感触を掴もうと考えていたら意外と時間がかかってしまった。時間がかかった理由としては、作法があるのでそれを学ぶまではBackbone.jsがどういう物なのか理解できなかったのと、作法を覚える事が若干多い。Router周りは良いドキュメントが見つからなかったので苦労した。(まあ、想像通りの作りだったが) 最後は色々検証する為にサーバサイドを用意するのが必要だった事。

 そんなわけで敷居が高めなので導入には若干苦労するかも。覚えてしまえば他のフレームワーク同様生産性は上がるのだろうけど。

 さて、Backbone.js自体の構成としてはjQueryではなく、UNDERSCORE.js強く依存して作られている。UNDERSCORE.jsは便利関数群でその名の通りjQueryの「$」ではなく「_」を使う。(例:_.each([1,2,3], function(n){alert(n);})) 別にjQueryと競合するわけではないので、アプリ側としては今までどおりjQueryを使うことは出来る。というかBackbone.js自体も一部の機能を実装にjQueryを使っていてはいる。(zepto.jsを選択することもできるらしい) モジュールの構成としてはEvents、Model、Collection、Router、History、Sync、View、Utilityがある。

  • Events:カスタムイベントをバインドすることが出来る(jQueryと同じ様にon、offで付けたり外したりして、triggerで発火)
  • Model:肝となるモジュール。サーバサイドとの通信、バリデーション、アクセスコントロールを行う。
  • Collection:Modelを管理する。Modelの値の変更、追加、削除イベントを管理する。
  • Router:アプリのURLと機能を管理する。(/user/でユーザ情報を表示、/item/でアイテム情報表示等)
  • History:pushStateかサポートしないブラウザはハッシュでURLの変更を管理する。(GitHubのディレクトリブラウジングの様にアドレスは変わるがリロードしない奴)
  • Sync:Modelを読み込みや保存時に呼ばれる関数。(場合によってはAjax、場合によってはlocalStorage等)
  • View:HTMLやCSS周りを処理するわけではなく、ビューの処理を書く場所。(テンプレートエンジン等をここで使ってHTMLを作ったりする)
  • Utility:jQueryと同じnoConflictとsetDomLibraryで利用するjQueryを指定出来る。

 Backbone.jsの記述の仕方として基本以下の形で書いていく。

Backbone.XXX.extend(option);

 サンプルとしては以下の様な感じ。

// ユーザModelを作成
var User = Backbone.Model.extend({
    clear: function() { // 削除する場合はdestroyしてHTMLタグを消す
		var el = this.el;
		this.destroy({success: function() {
			$(el).remove();
		}, wait: true});
	}, url: function() {
		return '/user/' + this.id;
	}
});
// ユーザCollectionを定義
var UserList = Backbone.Collection.extend({
    model: User,
	url: '/user/'
});
var Users = new UserList;
// ユーザViewを定義
var UserView = Backbone.View.extend({
    tagName: 'tr' // 全体を囲うタグをtrとする
	, template: _.template($('#user-template').html()) // テンプレート
	, events: { // tr以下に作成されるタグのイベントと関数指定
		'dblclick .view'    : 'edit'
		, 'keypress .edit'  : 'updateOnEnter'
		, 'blur .edit'      : 'close'
		, 'click a.destroy' : 'clear'
	}, initialize: function() {
		this.model.on('change', this.render, this);
		this.model.on('destroy', this.remove, this);
	}, render: function() {
		this.$el.html(this.template(this.model.toJSON()));
		this.name = this.$('.edit[name=name]');
		this.email = this.$('.edit[name=email]');
		return this;
	}, close: function() {
		var name = this.name.val();
		var email = this.email.val();
		this.model.save({name: name, email: email});
		this.$el.removeClass('editing');
	}, edit: function() {
		this.$el.addClass('editing');
	}, updateOnEnter: function(e) {
		if (e.keyCode == 13) this.close();
	}, clear: function() {
		this.model.clear();
	}
});
// アプリViewを作成
var AppView = Backbone.View.extend({
	el: $('#userapp')
	, initialize: function() {
		this.name = this.$("#new-name");
		this.email = this.$("#new-email");
		Users.on('add', this.addOne, this);
		Users.on('reset', this.addAll, this);
		Users.on('all', this.render, this);
		Users.fetch();
	}, events: {
		'keypress #new-name': 'createOnEnter',
		'keypress #new-email': 'createOnEnter'
	}, addOne: function(user) {
		var view = new UserView({model: user});
		this.$('#user-list').append(view.render().el);
	}, addAll: function() {
		Users.each(this.addOne);
	}, createOnEnter: function(e) {
		if (e.keyCode != 13) return;
		if (!this.name.val()) return;
		Users.create({name: this.name.val(), email: this.email.val()});
		this.name.val('');
		this.email.val('');
	}
});
// Routerを定義
var AppRouter = Backbone.Router.extend({
	routes: { // アドレスに対応する処理関数の定義
		'help': 'help' // /help の場合は下のhelp関数を呼ぶ
		, '': 'user'   // / の場合は下のuser関数を呼ぶ
	}, help: function() {
		$('#userapp').html($('#help').html());
	}, user: function() {
		$('#userapp').html($('#user').html());
		Users.fetch();
	}
});
var app = new AppRouter();
Backbone.history.start({pushState: true});

var App = new AppView;

 他のテンプレートエンジンを使うことも可能だが、テンプレートは以下のように書ける。 今回はtableタグユーザ一覧を表示し1Modelはtrで表示する事にした。

f:id:wata_htn:20120523001319p:plain

<script type="text/template" id="user-template">
    <td class="view">
		<span><%= name %></span>
		<input type="text" name="name" value="<%= name %>" class="edit">
	</td>
	<td class="view">
		<span><%= email %></span>
		<input type="text" name="email" value="<%= email %>" class="edit">
		<a class="destroy"></a>
	</td>
</script>

 こんな感じで定義しておけば、登録、更新、削除で非同期でサーバサイドに送られるし、HTML的にも勝手に、追加、更新、削除される。

 サーバサイドとの連携で設定出来る項目が2つある。どちらもREST APIに対応出来ない場合の設定。

Backbone.emulateHTTP = true;

 メソッドとしてGET、POST、DELETE、PUTの代わりにGETとPOSTだけにする。そのかわりにリクエストヘッダの「X-HTTP-Method-Override」でRESTとしてのメソッドを送信する。

Backbone.emulateJSON = true;

 これはリクエストの「application/json」に対応していない場合に「application/x-www-form-urlencoded」としてエンコードして送信する。

 両方がtrueの場合は「_method」パラメータでメソッドも送られるようになる。

 RouterでpushStateを使った場合は当然サーバサイドで「/help」を直接アクセスされても大丈夫なように作成する必要がある。具体的には「/」で来ようが「/help」で来ようが同じhtmlにたどり着くようにしておくということ。

 まだまだ試した事とか細かい事があるが、多すぎるので割愛。Backbone.jsを使った感想としては、「MVCっぽい試みをJavaScriptで本格的にやってみた。」という印象。もう少しルールベースを取り入れて記述用を減らしてもいいかもしれない。後はどこでどんな処理をするかは、結構色々書けたりするのでコーディング規約的なのを決める必要がありそう。その為にはある程度使いこなしてグッドプラクティスを詰めていく必要があるが・・・。データのやりとりをRESTで、HTML部分を見事に切り出してModelに紐付けたのはすごいと思った。この分野も、もう少し成熟して来ると本格的にみんなが使い出していくかもしれない。