jQuery Mobileでのアプリケーション開発にBackbone.jsを導入しよう
こんにちは、2011年度新卒エンジニアの夏目です!突然ですがみなさんJavaScript書いてますか? 最近はjQuery Mobileなどを利用したスマートフォン向けアプリ開発において、クライアントサイドでもヘビーなJavaScriptのコーディングをする機会があると思います。そのようなときコードのいたるところにHTMLが混入したり、どこでどのデータを扱っているのか分からなくなるということになりがちです。
今回はそんな悪夢のようなコーディング生活に一筋の希望の光を照らすBackbone.jsを紹介したいと思います。
対象読者
- JavaScriptでの開発経験がある方
Backbone.jsとは
Backbone.jsはDocumentCloudが開発をしている、クライアントサイドのJavaScriptコードをModel、View、Controllerで構築するためのフレームワークです。backboneとは日本語で「背骨」という意味で、Backbone.jsは背骨のようにコード全体を支える役割を担ってくれます。
そもそもMVCとはソフトウェア設計の一つで、アプリケーションのデータとその手続をModel、それらのデータの取得・表示をView、入力の処理をControllerが行うという構造です。
Backbone.jsのModelはgetやsetでデータの取得やセットができ、save、destroyで指定したurlにクエリを送信します。
CollectionはaddやgetでModelの追加、取得ができ、fetchで指定したurlのデータを取得し、parseでそのデータをそれぞれのmodelに格納するという流れです。
上記のデータをViewで実際にDOMへの操作を行います。Viewにはel要素があり、そこに操作したいHTML要素を指定します。elを元にデータの操作やイベントの登録を行います。
また、ViewにはdelegateEventsという機能があり、Viewの初期化時にイベントを登録することができます。
Backbone.jsではViewはControllerの一種だったり、UIから発生するイベントの実行などとしても機能します。
詳しくは公式のドキュメントを確認してください。
簡単なサンプルアプリ
まずはBackbone.jsを実際に体験するために、Model、Collection、Viewを利用して、クリックするたび友だちが増える簡単なサンプルを作ってみたいと思います。
このサンプルは下図に示したように、ボタンをクリックすると名前を保持したModelを作成し、それをCollectionにaddするとViewが画面に描画するという形で実装します。

準備
Backbone.jsは同社が開発しているUnderscore.jsに依存しているためそれぞれダウンロードします。 Underscore.jsはユーティリティライブラリで、JavaScriptでのプログラミングのサポートをしてくれる便利なメソッドが用意されています。 さらにajaxのリクエストやDOMの操作でjQueryやZeptoを利用しているのでどちらか好きな方を用意します。今回はjQuery Mobileを利用する都合上jQueryを利用したいと思います。
実装
それでは実際にコーディングしてみましょう。
friends.js
var Friend = Backbone.Model.extend({
// 作成日時を保持
initialize: function() {
this.set({date: new Date()});
}
});
var Friends = Backbone.Collection.extend({
model: Friend
});
var FriendView = Backbone.View.extend({
el: "#friends",
events: {
"click button": "addFriend" // #friends要素以下のbuttonにclickイベントを登録
},
initialize: function() {
this.collection = new Friends();
this.collection.bind("add", this.render, this); // collectionにaddされたらrenderを実行
},
render: function(friend) {
$(this.el).children("ul").append(this.template(friend));
},
// friendを作成し、collectionに追加する
addFriend: function() {
var rand = Math.floor(Math.random()*this.nameTemplate.length);
var name = this.nameTemplate[rand];
var friend = new Friend({friendName: name});
this.collection.add(friend);
},
template: function(friend) {
return "<li>"+friend.get("friendName")+"</li>";
},
nameTemplate: [
"山田",
"小鳥遊",
"種島",
"伊波",
"轟",
"白藤",
"佐藤",
"相馬",
"音尾"
]
});
index.html
<html>
<head>
<meta http-equiv="Content-type" content="text/html; charset=utf-8">
<meta name="viewport" content="width=device-width,user-scalable=no,initial-scale=1">
<title>hello</title>
<script type="text/javascript" src="lib/js/underscore-min.js"></script>
<script type="text/javascript" src="lib/js/jquery-1.6.4.min.js"></script>
<script type="text/javascript" src="lib/js/backbone-min.js"></script>
<script type="text/javascript" src="js/friends.js"></script>
<script type="text/javascript">
$(function(){
var view = new FriendView();
});
</script>
</head>
<body>
<div id="friends">
<button>クリック</button>
<ul>
</ul>
</div>
</body>
</html>
デモページを用意したのでご覧ください。
このようにModelとViewを分けることで非常にわかりやすいコードになります。
Tumblrの投稿取得アプリをBackbone.jsで
では次に前回のjQuery Mobileの紹介記事でデモとして作成したTumblrの投稿を取得するアプリを、Backbone.jsのModel、Collection、View、とUnderscore.jsのtemplateを利用して実装してみたいと思います。またBackbone.jsにおけるControllerはurlでコントロールを行うRouterというものがあるのですが、今回はjQuery Mobileを利用してViewの切り替えを行なっているのでここでは利用しません。
下図のようにユーザ名の入力画面、検索結果画面、検索結果の詳細画面それぞれにViewを作り、入力されたユーザ名の投稿一覧をPostのCollectionに格納し、検索結果画面でCollectionを一覧表示、検索結果の詳細画面では選択されたModelを表示するといった設計にしたいと思います。

実装
では実装してみましょう。
tumblr.js
$(function(){
var Post = Backbone.Model.extend({
// TumblrのPostから適切なタイトルを取得する
getTitle: function() {
var text;
switch (this.get("type")) {
case "regular": { text = this.get("regular-title"); break; }
case "link": { text = this.get("link-text"); break; }
case "quote": { text = this.get("quote-source"); break; }
case "photo": { text = this.get("photo-caption"); break; }
case "conversation": { text = this.get("conversation-title"); break; }
case "video": { text = this.get("video-caption"); break; }
}
return this.splitTags(text) || "no title";
},
// TumblrのPostから詳細データを取得する
getDetail: function() {
var content;
switch (this.get("type")) {
case "regular": {
content = this.get("regular-body");
break;
}
case "link": {
content = this.linkTemplate({url: this.get("link-url"), text: this.get("link-text")});
break;
}
case "quote": {
content = this.get("quote-text") + "<br>" + this.get("quote-source");
break;
}
case "photo": {
content = this.imgTemplate({url: this.get("url"), img: this.get("photo-url-400"), caption: this.get("photo-caption")});
break;
}
case "conversation": {
content = this.get("conversation-text");
break;
}
case "video": {
content = this.videoTemplate({video: this.get("video-player"), caption: this.get("video-caption")});
break;
}
}
return content || "no content";
},
splitTags : function(text) {
return text.replace(/<\/?[^>]+>/gi, "");
},
linkTemplate: _.template("<a href='<%= url %>' data-role='button'><%= text %></a><br>URL: <%= url %>"),
imgTemplate: _.template("<a class='src' href='<%= url %>'><img class='img' src='<%= img %>' /><span class='caption'><%= caption %></span></a>"),
videoTemplate: _.template("<%= video %><span class='caption'><%= caption %></span>")
});
var Posts = Backbone.Collection.extend({
model: Post,
// Collectionのurlを設定
initialize: function(data) {
this.url = "http://" + data.userId + ".tumblr.com/api/read/json";
},
// urlから取得したデータからModelになる部分を返す
parse: function(resp) {
return resp.posts;
}
});
window.SearchPage = Backbone.View.extend({
el: "#search",
events: {
"click button": "search"
},
// 検索結果ページにユーザIDを渡して画面遷移させる
search: function() {
var userId = $("#user-id").val();
if(userId){
if(typeof page.resultPage === "undefined"){
page.resultPage = new ResultPage(userId);
} else {
page.resultPage.reset(userId);
}
page.resultPage.render();
$.mobile.changePage("#result");
}
}
});
window.ResultPage = Backbone.View.extend({
el: "#result",
events: {
"click li": "changePage"
},
initialize: function(userId) {
this.reset(userId);
},
render: function(userId) {
// JSONP形式でcollectionのurlに非同期通信を行う
this.collection.fetch({dataType: "jsonp", success:$.proxy(this.add, this)});
return this;
},
// collectionのfetchが成功したらlistにそれぞれのPostをappendしていく
add: function(collection, resp) {
var that = this;
var list = $("#post-list");
collection.each(function(post, key) {
list.append(that.template({text: post.getTitle(), id: post.cid}));
});
list.listview("refresh");
},
// DOMとcollectionをリセット
reset: function(userId) {
$("#post-list").empty();
$(this.el).find(".user-id").text(userId);
this.collection = new Posts({userId: userId});
},
template: _.template("<li id='<%= id %>'><a><span class='text'><%= text %></span></a></li>"),
// 検索結果の詳細ページに遷移する
changePage: function(event) {
var id = event.currentTarget.id;
if(typeof page.resultDetailPage === "undefined") {
page.resultDetailPage = new ResultDetailPage();
}
page.resultDetailPage.render(this.collection.getByCid(id));
$.mobile.changePage("#detail");
}
});
window.ResultDetailPage = Backbone.View.extend({
render: function(post) {
this.model = post;
$("#post-title").text(this.model.getTitle());
$("#post-content").html(this.model.getDetail());
return this;
}
});
});
index.html
jQuery Mobileを利用し#search、#result、#detailというページを用意します。前回と変わったことろはtumblr.jsを読み込み、SearchPageをnewするということだけです。
<html>
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width,user-scalable=no,initial-scale=1">
<title>tumblr投稿閲覧</title>
<link rel="stylesheet" href="../lib/css/jquery.mobile-1.0rc1.min.css" />
<script type="text/javascript" src="../lib/js/underscore-min.js"></script>
<script type="text/javascript" src="../lib/js/jquery-1.6.4.min.js"></script>
<script type="text/javascript" src="../lib/js/backbone-min.js"></script>
<script type="text/javascript" src="js/tumblr.js"></script>
<script type="text/javascript" src="../lib/js/jquery.mobile-1.0rc1.min.js"></script>
<script type="text/javascript">
$(function(){
window.page = {};
page.searchPage = new SearchPage();
});
</script>
</head>
<body>
<div data-role="page" id="search">
<div data-role="header">
<h1>tumblr投稿閲覧</h1>
</div>
<div data-role="fieldcontain">
ユーザー名: <input type="search" id="user-id">
<button>検索</button>
</div>
</div>
<div data-role="page" id="result" data-add-back-btn="true" data-back-btn-text="戻る">
<div data-role="header">
<h1>結果 : <span class="user-id"></span></h1>
</div>
<ul data-role="listview" id="post-list" data-inset="true">
</ul>
</div>
<div data-role="page" id="detail" data-add-back-btn="true" data-back-btn-text="戻る">
<div data-role="header">
<h1 id="post-title"></h1>
</div>
<div data-role="content" id="post-content"></div>
</div>
</body>
</html>
実行すると以下のようになります
こちらもデモページを用意したので、ぜひ試してください。
Backbone.jsを利用してコードをMVCに切り分けることで、見やすく簡潔になりました。
まとめ
スマートフォンアプリの開発にJavaScriptを導入することも多くなり、みなさんもJavaScriptに触れる機会が増えているかと思います。そのときはBackbone.jsを導入してハッピーなコーディングライフを送ってみてはいかがでしょうか。
TrackTube
著者: