大規模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>
scriptタグのtype='text/template'でHTMLテンプレート書く

Mustacheで書いて$('#hoge_template').html()で取り出して使う
scriptタグの中にHTMLを書くのは、必要になるまでDOMのインスタンスを作らせないようにするため。

このパターンが実際にどう開発に影響を与えるか?

モデル

テストが書きやすくなり、充実する。
自分のプロジェクトではモデルがDOMに一切依存しないため、nodeで動かしている。

ビュー

Backbone.Viewのテストをする場合、各テストケースのセットアップでグローバルなDOMを初期化しないといけない。このモックHTMLを書くのが非常に手間で、さらにいえば、現代のHTMLは(残念ながら)デザインと不可分であるため、デザイン変更によってセレクタがずれたり、親子関係が入れ替わったりすることがある。よって、どんなにテストを書いても無駄になることはある。可能な限りidやセレクタを振って、親子関係に依存しないマークアップが求められる。

副作用をもつメソッド群をスタブして、発火してることを確かめるのは有意義。逆に言えばそれ以上のテストはあまり有意義でない。

最終的に何が言いたいかというと

jQuery黒魔術つらい