- HTML要素上のデータ
- ブラウザメモリ上のデータ
- サーバサイドのデータ
というわけで、そんな状況を打破するためのクライアントフレームワークがここ2~3年で大変充実してきていて、今回解説するBackbone.js、もちっとお手軽らしいEmber.js、もっともっとシンプルなKnockout.jsなどなど、どれから手にとって良いやら迷うこと必至なほど盛況だ。で、とりあえず選考の一助になればと簡単なサンプルアプリケーションを作成してみた。
今回のアプリケーションの外観は下図。ソースコードはhttps://github.com/yooontheearth/backbone-tutorialから取得可能になっている。
ご覧の通りよくある一般的な尼子十勇士系のアプリケーションだ。左側のリストを選択するとその詳細が右側に表示され、追加、編集、保存が可能となっている。今回のサンプルではBackbon.jsにフォーカスしたかったのでサーバサイドでの永続化処理(DBへの保存など)は行っていない。
アプリケーションの環境
今回のアプリケーションの環境は下記の通り。
- サーバ Ubuntu@12.04
- Webサーバ Node.js@0.6.9
- サーバフレームワーク Express.js@2.5.0
- Viewエンジン Jade@0.26.0
- CSSエンジン Stylus@0.23.0
- クライアントフレームワーク Backbone.js@0.9.2
- CoffeeScript
Node.jsのセットアップ方法などはNode.jsをUbuntu 11.10にセットアップする ついでにExpress.jsもセットアップするなどを参考にしてもらいたい。
ソースコードをどこか適当な場所に$ git cloneなりダウンロードして展開したら下記コマンドで必要なモジュールをインストールする。
$ npm install -d
package.jsonに記述してあるもので必要なモジュールはすべてそろっていると思うけれど、サンプルを作った環境はグローバルにたくさんモジュールをインストールしすぎていて何が必要なのかよく分からなかったので少し適当だ。実行時にエラーが出たらその都度必要なモジュールを追加してほしい。今回はコーディングをCoffeeScriptで行っているので下記コマンドでCoffeeScriptをグローバルにインストールする。$ npm install coffee-script -g
これで準備が整ったので下記コマンドでアプリケーションを実行してみよう。$ NODE_PATH=/usr/local/lib/node_modules coffee app
"Express server listening on port 3000 in development mode"とかなんとか表示されていたら正常に起動できたので下記URLへアクセスしてみよう。
http://localhost:3000/index
先ほどの尼子十勇士的なリストが表示されていたら正常に動作している。Backbone.jsの解説
Backbone.jsは大きく3つのパートに分けられる。
- Model,Collectionのデータ系
- Viewの表示系
- Routerの管理系
今回のアプリケーションで設定されているRouteは下図の通り。
Route | 動詞 | 説明 |
---|---|---|
/index | get | 初期表示を行う |
/list | get | リストデータを取得する。Collection.fetch()でリクエストされる。今回は実装していないけれど本来的にはページングなどを行う |
/busho | post | 武将情報の新規追加を行う。Model.save()時にModelが新規(Model.isNew())に作成されたものの場合はpostリクエストされる |
/busho/:id | put | 武将情報の更新を行う。Model.save()時にModelが新規でないものの場合はputリクエストされる |
/busho/:id | delete | 武将情報の削除を行う。Model.destroy()でリクエストされる |
ここから個別にBackbone.jsを使ったコーディングを見ていこう。ソースコードは./public/javascripts/index.coffeeを参照してもらいたい。
データ系
# 武将モデル Busho = Backbone.Model.extend urlRoot: 'busho' # Bushoモデルがサーバへのリクエストを行うときの基本となる部分の指定 defaults: # templateで使用するプロパティがundefinedだとエラーになるので初期値を設定しておく id:null name:null description:null imageUrl:null initialize: -> @on 'error', @failOnValidation @on 'destroy', @close validate: (attrs) -> if not attrs.name? or attrs.name.length == 0 return '名称は必須です' # 検証に失敗した場合はメッセージを返す failOnValidation: (model, error) -> alert error # 検証に失敗した場合のイベントハンドリング close: -> @off()ここで指定しているurlRootの部分が前述のRouteの部分(/busho, /busho/:id)と対応するようになる。
# 武将モデルコレクション BushoList = Backbone.Collection.extend model: Busho # createを呼び出す場合はmodelの指定は必須 url: 'list' # fetchするときのリクエスト先ここで指定しているurlの部分が前述のRouteの部分(/list)と対応するようになる。
ビュー系
# リストビュー ListView = Backbone.View.extend tagName: 'table' initialize: -> # modelはBushoListなのでBushoListが変更されたときに備えてイベント登録 @model.bind 'reset', @render, this @model.bind 'add', (busho)=> $(@el).append new ListItemView(model:busho).render().el # tableに新しい行を追加する render: -> $(@el).empty() _.each @model.models, (busho) -> # BushoListの内容をtableに行として追加する $(@el).append new ListItemView(model:busho).render().el , this return thisこのビューはリスト=CollectionをModelとして受け取り、Collectionのデータ構造をUI的に表すのと、Collectionの変化(リストの更新、追加など)をUIへと反映させる責任を持つ。またtagNameはビューで作成される要素を表しデフォルトではDIVになり、elはその作成された要素を表す。
# リストビューアイテム ListItemView = Backbone.View.extend tagName: 'tr' template: _.template $('#tpl-list-item').html() initialize: -> # modelはBushoなのでBusho情報が変更されたときに備えてイベント登録 @model.bind 'change', @render, this @model.bind 'destroy', @close, this render: -> $(@el).html @template @model.toJSON() return this events: 'click td':'select' # 行が選択された処理をフックするためにUI要素のイベント登録を行う select: -> $('tr.selected').removeClass('selected') $(@el).addClass('selected') app.navigate "busho/#{@model.id}", true # 詳細情報を表示するためにルーターに通知する close: -> $(@el).off().remove()templateプロパティへビューに表示するHTML要素を設定する。今回はUnderscore.jsのテンプレートを使用しているけれど、もちろんjQueryTemplateでもなんでもかまわない。ただここにHTML要素をだらだらと記述するのは関心の分離を進めるMVC的には正しくないのでテンプレートを使用するようにしよう。eventsプロパティに要素内でフックしたいイベントの記述をする。構文的には'イベント セレクタ':'イベントハンドラ'となる。
# 詳細情報画面 DetailsView = Backbone.View.extend template: _.template $('#tpl-details').html() render: -> $(@el).html @template @model.toJSON() $(@el).find('#delete').hide() if @options.hideDelete return this events: 'change input,textarea':'changeData' # UI要素に入力された情報をモデルに反映するためのイベントハンドリング 'click #save':'save' 'click #delete':'delete' changeData:(event)-> # changeイベントでデータを必ずしも反映させる必要はなく、アプリの要件によっては保存ボタン押下時などにまとめて行っても良い changeDataSet = {} changeDataSet[event.target.name] = event.target.value @model.set changeDataSet, silent:true # silent:trueで検証を無効化している save: -> if @model.isNew() # 新規登録時はリストに追加する app.list.create @model, wait:true success: (model, response) => @model.set 'id', app.list.length # IDの採番は適当。本来ならばサーバからのレスポンスにIDを渡しておいて設定するとか app.navigate '', true error:(model, error)-> # サーバサイドでのエラーはresponseTextを参照、それ以外はクライアントでの検証エラー alert if error.responseText then error.responseText else error else @model.save {}, success: (model, response) -> # とくに処理なし error: (model, error) -> alert if error.responseText then error.responseText else error return false delete: -> return false unless confirm '削除しますか?' @model.destroy success: -> app.navigate '', true return false close: -> $(@el).off().empty()save時に新規の場合はリストに要素を追加したいのでapp.list.createとしているけれど、Backbone.jsの内部的にはModel.save()が呼び出されている。
# ヘッダービュー HeaderView = Backbone.View.extend template: _.template $('#tpl-header').html() render: -> $(@el).html @template() return this events: 'click #refresh':'refresh' 'click #add':'add' refresh: -> app.navigate 'refresh', true return false add: -> app.navigate 'busho/add', true return falseRouterへの通知を行うだけのビュー。
管理系
AppRouter = Backbone.Router.extend routes: 'busho/add':'add' 'busho/:id':'details' 'refresh':'list' '':'closeDetails' initialize: -> @list = new BushoList() @list.reset _bushoList # 初期表示するデータはページロード時に用意してあるのでそちらから取得 @listView = new ListView model:@list $('#list').html @listView.render().el $('#header').html new HeaderView().render().el # ヘッダービューをDOMツリーに反映 add:-> @detailsView.close() if @detailsView @detailsView = new DetailsView model:new Busho() hideDelete:true # 追加なので削除ボタンは不要 $('#details').html @detailsView.render().el # 詳細ビューをDOMツリーに反映 details:(id)-> @detailsView.close() if @detailsView busho = @list.get id # リストから詳細を表示するアイテムを取得 @detailsView = new DetailsView model:busho $('#details').html @detailsView.render().el # 詳細ビューをDOMツリーに反映 list:-> @list.fetch() # リストの更新 # ※fetch({data: {page: 3}}) のような形でQueryStringを渡せるのでページングなどもfetchで行える closeDetails:-> @detailsView.close() if @detailsView # 詳細ビューが開いていたら閉じるroutesプロパティにクライアント側でページの遷移を制御するためのRouteを登録しておく。Routerはroutesプロパティに基づいてリクエストされた処理を行い、クライアントとサーバの仲立ちを行う。
呼び出し
app = null $(document).ready -> app = new AppRouter() Backbone.history.start()使用方法はいたって簡単。AppRouterをインスタンス化するだけ。あとはRouterがよきように取り計らってくれる。
まとめ
ここまで駆け足で説明してきたけれどいかがだろうか?今回のサンプルではデータの永続化処理を行っていないので微妙な部分も多々あるけれど、Backbone.jsを使うとクライアントとサーバ、ビューとデータ、UI操作とデータ処理の分離が上手に行えるのが分かったかと思う。ただBackbone.jsを使ったとしてもアプリケーションの規模が大きくなってくれば必然的にコード量が増えるのでどうしても管理は煩雑になっていき適切なサブモジュール化などを行う必要が出てくるけれど、そのサブモジュール化自体をBackbone.jsのおかげで容易に行える実感は持てたのではと思う。UI要素とデータの結びつきとデータの更新に伴うUI要素への反映をさっくりとできるBackbone.jsはどのような場面でも非常に有用なので是非活用してもらいたい。
0 件のコメント:
コメントを投稿