大規模JSでのBackbone.js/CoffeeScript について考えてみた
これ読んでたらr7kamura君にJSのMVCどうするの的な話きかれてたのを思い出したので、自分がBackboneを使う時のパターンをr7kamura君の記事をベースに書きなおしてみた。
> サバクラ両方で動く JavaScript の大規模開発を行うために ― Gist https://gist.github.com/1362110
> client-side javascript - ✘╹◡╹✘ http://r7kamura.hatenablog.com/entry/2012/10/18/023629
以下の様なコードを書いた。かなり冗長だが、複雑なアプリだとこれぐらいの冗長性は必要になる。
(なお概念を伝えるための解説用コードなのでそのままじゃ動かない)
Backbone.Model
# 名前空間の初期化 App = {} App.View = {} App.Model = {} # Backboneを継承して実装する class App.Model.TabSelecter extends Backbone.Model defaults: index: 0 next: -> index = @get 'index' @set index: index + 1 prev: -> index = @get 'index' if index > 0 @set index: index - 1
ViewModelを意識
モデルのデータが変化するロジックは全てモデルに書く。この場合はビューに関するデータをもっているので、実質ビューモデルといっていい。
Backbone継承パターン
Backbone.Modelは複雑な振る舞いを持つことが多い。これらはコントローラ側で制御せず、モデル自身が厚く振る舞いを持てるようにする。そうでないとコントローラが肥大化して手に負えなくなる。
外部からの get, setは控えめに
どこでsetするかを明示的にするために、可能な限り外部からsetすることは避ける。むしろgetも禁止して取り出したい項目はgetter作らないと取得できない、ぐらいの拘束でもいい。
理由は、外部から複雑なデータの受け渡しを行った場合、どこでデータが加工されたかわからなくなるケースが多々ある。
Backbone.View
Viewって名前だけど実際はコントローラだよ!
class App.View.Tabs extends Backbone.View el: '.tabs' events: 'click .button': 'select_tab' 'click #next': 'next' 'click #prev': 'prev' constructor: -> super @tab_selecter = new App.Model.TabSelecter @tab_selecter.on 'change:index', 'render' @tab_views = for el, index in $('.tab') # 冗長だが実際には個別のテンプレートだと思われるので # indexでテンプレートを分岐する new App.View.TabItem el: el, template: do -> switch index when 0 then $("#a_template") when 1 then $("#b_template") when 2 then $("#c_template") next: -> @tab_selecter.trigger 'next' prev: -> @tab_selecter.trigger 'prev' current_tab: -> @tab_views[@tab_selecter.get('index')] select_tab: (event, el) => # click eventを受け取る # DOMイベントから必要な値を取り出す tab_index = $(el).index $('.tab') @set index: tab_index render: => # => でthisをbind @current_tab().render_to $('.section')
「モデルによって一意に決まる状態」を意識する。
タブを選択する、という場合、indexがすべての状態を一意に決めることができるので、index状態の変化を監視してrenderを呼ぶ。ビューはDOMを書き換えず、モデルを監視した結果副作用(#render)を起こす。副作用を起こすメソッドと監視するメソッドは明確に分離する。
イベントハンドラとして振る舞う関数はHTMLからデータを読み取ってモデルのメソッドを叩く。
render系のメソッドはDOMへの副作用を隠蔽する。
bindされたeventはDOMのパラメータを取り出す以上の事はしない
clickされたDOMからdata属性を取り出したり、現在のDOMの状態をみたりはするが、それ以上のロジックは自分自身に持たず、モデル側に記述する。ただし、モデルはDOMに関する情報(id情報やDOM)を持たない。
操作の結果、モデルが書き換わり、上記のデータによって一意に決まる状態が更新される。そのために、モデルが一意に決まる状態を持たねばならない。
可能な限りeventsを使ってイベントを定義する。
eents経由(正確には#delegateEvents)で定義したものは、Backbone.View#undelegateEvents() で解放できる。ビューを作っては破棄していると、イベントのバインド漏れで複数イベントが飛んだり、メモリリークを起こす可能性が高くなる。
HTMLをラップするView
#.tabをラップするクラス class App.View.TabItem extends Backbone.View constructor: -> super @$el.html Mustache.render @template, {} hide: -> @$el.hide() show: -> @$el.show() render_to: ($section) -> $section.html @$el
this.$elをラップしてjQueryのアクションに名前をつける
HTMLと強く紐付くクラスは、@$elをラップして振る舞いを記述する。jQueryでこねくり回す場合でも、名前をつけて意味的な名前をつけると、意味を見失いがちなjQueryの一連の振る舞いをわかりやすくすることができる。
this.$elを使いまわす
$elはイベントを画面から取り除かれてもイベントを保持し続ける。大きなDOMを再生成するコストを省ける。
「本当の」View
実体はHTMLテンプレート
<!-- templates --> <script id='a_template' type='text/template'> {{a}} </script> <script id='b_template' type='text/template'> {{b}} </script> <script id='c_template' type='text/template'> {{c}} </script>
このパターンが実際にどう開発に影響を与えるか?
モデル
テストが書きやすくなり、充実する。
自分のプロジェクトではモデルがDOMに一切依存しないため、nodeで動かしている。
最終的に何が言いたいかというと
jQuery黒魔術つらい