大規模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黒魔術つらい

should.jsが辛いのでやめたくなった

Rspec風に使える、という理由で使ったけど、もうなんというか気分的に辛い
visionmedia/should.js https://github.com/visionmedia/should.js/

問題1 nodeのassert依存

ブラウザ用に移植できない

問題2 undefined, null は prototypeを持たない

次のようなコードは getHogeがundefined返してしまうと hoge.shouldを触った時点で落ちる

hoge = getHoge()
hoge.should.equal 'hoge'

アサーションにすらたどり着けず落ちるのはストレスたまる
やっぱラップするタイプのexpectの方がよさそう

問題3 nodeのネイティブモジュールに依存したオブジェクトはprototypeを共有していない

JSDOMで生成したオブジェクトのアサーションができない!!!

結論

chai使いましょう

Home - Chai http://chaijs.com/

[coffee-script] coffee-scriptでrubyのModuleっぽいものを実装してみた

とはいっても関数1つだけど。

ここしばらくずっとtypescript書いてる。
typescriptのmoduleをコンパイルされたコードみてたら、名前空間がなかったら作る、あったら何もしない。と、それだけの実装だったので普通の関数として実装してみた。

root =
  if window? then window
  else if global? then global
  else this

root._module_ = (ns, f) =>
  context = root
  hist = []
  for name in ns.split('.')
    unless context[name]?
      context[name] = {}
    context = context[name]
    hist.push context
  f.apply context, hist

ドットで区切られた名前空間ごとの参照は、引数で受け取る事ができる

こんなふうに使うことを想定している

# how to use
_module_ 'App', (App) ->
  class @Main
    constructor: ->
      App.instance = @
      @hoge_model = new App.Model.Hoge
      @hoge_view = App.View.Hoge @hoge_model
  @instance = null

_module_ 'App.Model', (App, Model) ->
  class @Hoge

_module_ 'App.View', (App, View) ->
  class @Hoge
    constructor: (@model)->

new App.Main
console.log App.instance

require.jsは無駄に規約が多いのでこれぐらいがすっきりしそうに思った。関数1つだし。

Interfaceについて

typescriptのinterfaceの実装もしようとしたんだけど、あるオブジェクトがこのインターフェースを満たすか、っていうのは、言語レベルでもない限り、テストのヘルパでやれって気がしたので面倒になってやめた。実コードでやるものではない。
IEntity = Interface {x: Number, y: Number} って書きかけて飽きた。

Typescript、CoffeeScriptに比べるとやはり無駄に冗長なので、インデントやらなにやらだけcoffee-scriptなTypedCoffeeScriptみたいな言語が出たら最高だと思う。そこらへんの所感についてはあとで書く。

ウェブ業界の新卒が集まる勉強会行ってきた #oblove

ちょっと酔ったまま書いてるので色々アレですが何も書かないよりマシだと思ったので書きます。

オブラブ 収穫祭 〜若手エンジニア、実りの秋 http://esminc.doorkeeper.jp/events/1746


ウェブ業界の新卒(そんなものが存在するのか)が集まって、どんな業態で新卒がどんなふうに働いてるか発表する勉強会があると聞いて、一応ウェブ業界の新人枠で働いてる自分としては、冷やかしのつもりで行ってみた。そしたら適度に砕けてて適度に意識高かったのでよかった。

勉強会とその後の飲み会ではえらく意識高まったのだけど、個別の発表について感想書こうと思ったけどお酒入ってしまったせいで全然覚えてないし、お疲れ様回の後に飲み会の後 @ainame と @r7kamura (いずれも某社と某社の新卒) とまどまぎ後編見て感動したので、細かいことが吹き飛んでしまい覚えてない。

登壇していた企業は Cookpad, ペパボ, DeNa, GREE, 永和(敬称略 登壇順)


ウェブ業界の新卒って結局野良でコード書いてた学生集めてんだろと思ったんだけど、DeNaではポテンシャル採用でコード書いたことがない人もとってるらしく、永和の新人はプログラミング未経験でいきなりRailsやっててヤバイなーと思った。あとハッシュタグみてたらなんか名前だけ知ってる人たくさんいた。

印象としては企業の規模が大きくなると研修システムが機能しないと回らない、といった感じ。エンジニアの総数が少ないペパボ、CookPadはそもそも出来る人しか取らないので研修必要ないし、反対にGREE, DeNaはとにかく人集めない会社としてスケールできないので研修システムちゃんと作ろうと試行錯誤してるように見えた。

ただポテンシャル採用と言いつつもポテンシャルを測る指標が確立してないのはヤバイ感じがした。「苦労してる」以上の話は聞けなかった。


飲み会の時kwappaさんと話してたのだけど、エンジニアってそもそも教育が難しいので徒弟制度の方がいいんじゃないだろうか的な話をしていた。工房があって、マスターが一人いて、中堅達はいろんな工房で経験を積み、いずれはマスターになるのだけど、その下に新人がいて中堅が新人のメンターとしてみてて、みたいな。ここまで文章に落とすと大学の象牙の塔はまさにそうだなって思いだした。機能してるかどうかは別として。

全部の感想としては、まどかがすべての意識の高まりを引き受けて遠い宇宙にいったので、よく覚えていない。

クライアントJSをパッケージ管理できるjamjsが便利

nodeのnpmみたいなインターフェースで、クライアントjsの依存管理をする。

インストール

$ npm install -g jamjs

package.jsonのjamプロパティ以下に依存モジュール書く

{
  "name": "hogefuga",
  "version": "0.0.0",
  "jam": {
    "baseUrl": ".",
    "packageDir": "vendor",
    "dependencies": {
      "jquery": "1.7.x",
      "backbone": null,
      "underscore": null
    }
  }
}

package.jsonの定義に従ってダウンロード

$ jam install

自分はcompileまでする。

$ jam compile vendor/all.js

あとはみたいに読みこめば、jquery,backbone, underscoreが読み込み終わったのと同じ状態になる。

本来の使い方

自前のライブラリと連携させつつrequire.jsを使うといいらしいけど、個人的にはモジュール一つにつき汚す名前空間が1つならどうでもよくて、vendor/all.jsにまとめておければそれでいいと思った。

困ったこと 1

jamjsが内部で使っているnode-promptのAPIが変わってて、新しいパッケージを登録しようとした時に、prompt#readLineで落ちてしまっていた。変更後のAPIの名前がわからないのでとりあえずコード中にユーザー名とパスワード直打ちして解決した。悲しい。

困ったこと 2

どうしてもコントリビューターが少ないので登録パッケージが有名な奴しか無く、しかもjamjsの作者が勝手に登録しているものも多いので、勝手知ったる他人の庭感ある。(ライセンス的には問題ない)

enchant.jsを勝手に登録してみた。enchant.jsだとプラグインが大量にあるんだけどそれを全部登録するのもアレな気がするし、自分がマメにメンテするとも思えないので、必要な奴は適当に登録して、もしjamjs流行ったらその都度作者に譲渡していくのがいいと思っている。