How Batman can Help you Build Apps の翻訳

最近話題?のCoffeeScriptベースのフレームワークBatman.jsのHARRY BRUNDAGE(@harrybrundage)氏による紹介記事「How Batman can Help you Build Apps」を翻訳しました。
元エントリはこれです。

Batman.jsはShopifyによる新しいCoffeeScriptのフレームワークで、開発に長い時間のかかったこいつを紹介できるので本当に興奮している。Batman.jsのGitHubはここだ。
Batmanは大きな影響力を持つ素晴らしいフレームワークに満ちた世界に現れた。Sproutcore 2.0やBackbone.jsといった驚くべきプロジェクトの成果がある中で、開発者はどれをいつ使うかどうやって学べばいいのだろう? 新しくてかっこよいツールで遊んでいる時間は限られているので、ここではBatmanと他のフレームワークとの違い、および他のフレームワークのかわりにBatmanを使う理由を簡単に解説したい。

Batmanはアプリ開発を簡単にする
Batmanはシングルページのアプリを開発するためのフレームワークだ。Progressive Enhancement、DOM、AJAXなどの単一目的のライブラリではない。ブラウザ間互換、データ転送、バリデーション、カスタムイベントといった開発の退屈な部分を全て実装することで、最高のシングルページアプリを構築できるようゼロから開発されたものだ。コードを生成し実行する開発用の使いやすいヘルパー、コードを整理して必要に応じて呼び出すためのおすすめアプリ構造、フルMVCスタック、その他たくさんのツールを18キロバイト(gzip済み)のサイズで提供している。Batmanは基本的なものだけ提供するわけでも、ありとあらゆるものを提供するわけではなく、自分のアプリに必要なコードをかける柔軟なAPIを提供している。

超一級のランタイム
Batmanの心臓部はランタイム層で、オブジェクトのデータの操作とオブジェクトが発生させるイベントへの登録に使われている。BatmanのオブジェクトはSproutCoreやBackboneのものと似ていて、Barmanオブジェクトのプロパティに対するアクセスと代入は、素のJavaScriptのドット記法ではなくsomeObject.getとsomeObject.setを使わなければならない。これを守ることで、以下のような利点がある。

  • 「深い」プロパティが単純型でも計算型でも透過的にアクセスできる
  • プロパティチェーン中のオブジェクトの計算型のプロパティを継承できる
  • 「深い」パスの中の他のオブジェクトのchangeã‚„readyといったイベントに登録できる
  • なにより重要なのはプロパティ間の依存関係を追跡できるので、連鎖したオブザーバが発動され、計算結果を最新であることを保証しつつキャッシュできることだ

この機能はすべてのBatmanオブジェクト利用でき、しかも素のJavaScriptとしても扱うことができる。ランタイムでできることを少し試してみよう。オブジェクトのプロパティはBatman.Object::observeを使って監視できる。

crimeReport = new Batman.Object
crimeReport.observe 'address', (newValue) ->
  if DangerTracker.isDangerous(newValue)
    crimeReport.get('currentTeam').warnOfDanger()

似た機能はBackboneやSproutCoreにもあるが、Batmanに新たに取り入れたものが「深い」キーパスだ。Batmanではドットでつなげることでオブジェクトの連鎖を追跡できる。

batWatch = Batman
  currentCrimeReport: Batman
    address: Batman
      number: "123"
      street: "Easy St"
      city: "Gotham"
 
batWatch.get 'currentCrimeReport.address.number' #=> "123"
batWatch.set 'currentCrimeReport.address.number', "461A"
batWatch.get 'currentCrimeReport.address.number' #=> "461A"

オブザーバの指定時にも使える。

batWatch.observe 'currentCrimeReport.address.street', (newStreet, oldStreet) ->
  if DistanceCalculator.travelTime(newStreet, oldStreet) > 100000
    BatMobile.bringTo(batWatch.get('currentLocation'))

一番クレイジーなのはこれらのオブザーバはそのキーパスがなんであれ(たとえキーパスの途中が変化しても)、その値で発動することだ。

crimeReportA = Batman
  address: Batman
    number: "123"
    street: "Easy St"
    city: "Gotham"
 
crimeReportB = Batman
  address: Batman
    number: "72"
    street: "Jolly Ln"
    city: "Gotham"
 
batWatch = new Batman.Object({currentCrimeReport: crimeReportA})
 
batWatch.get('currentCrimeReport.address.street') #=> "East St"
batWatch.observe 'currentCrimeReport.address.street', (newStreet) ->
  MuggingWatcher.checkStreet(newStreet)
 
batWatch.set('currentCrimeReport', crimeReportB)
# 上記の"MuggingWatcher"のコールバックが「Jolly Ln」で呼ばれる

何が起きたかお分かりだろうか。キーパスの途中部が変化してもオブザーバは新しい「深い」値で発動する。この機能は任意の長さのキーパスでも、undefinedを含んだキーパスでも動作する。

もうひとつのランタイムのいいところは、すべてのアクセスがgetとsetを通じて行われるので、計算が必要なプロパティ間の依存関係を追跡できることだ。Batmanではこうした関数をアクセサと呼ぶ。アクセサは、CoffeeScriptの実行可能なクラスを使って簡単に定義できる。

class BatWatch extends Batman.Object
  # BatWatchクラスのインスタンスの「currentDestination」キーのアクセサを定義する
  @accessor 'currentDestination', ->
    address = @get 'currentCrimeReport.address'
    return "#{address.get('number')} #{address.get('street')}, #{address.get('city')}"
 
crimeReport = Batman
  address: Batman
    number: "123"
    street "Easy St"
    city: "Gotham"
 
watch = new BatWatch(currentCrimeReport: crimeReport)
watch.get('currentDestination') #=> "123 Easy St, Gotham"

重要なのは、ここで計算型のプロパティに登録したオブザーバは依存関係が更新されるたびに発動することだ。

watch.observe 'currentDestination', (newDestination) -> console.log newDestination
crimeReport.set('address.number', "124")
# "124 Easy St, Gotham"がコンソールに出力される

デフォルトのアクセサを定義しておけば、そのキーパスでのアクセサが定義されていなくても、ランタイムがフォールバックしてくれる。

jokerSimulator = new Batman.Object
jokerSimulator.accessor (key) -> "#{key.toUpperCase()}, HA HA HA!"
 
jokerSimulator.get("why so serious") #=> "WHY SO SERIOUS, HA HA HA!"

この機能はオブジェクトに基本のインターフェースを与えたいときに有用だが、自明ではない方法でデータと連動することになる。例えばBatman.Hashはイベントを発行しオブジェクトをキーとして使えるようにした上で、標準的なJavaScriptのオブジェクトと類似したAPIを提供するためにこの機能を使っている。

何に使えるのか?
上記で解説したBatmanのコアは、データの変更があったとき即座にそれを知ることを可能にする。クライアントサイドのビューなどには最適だ。ビューは、もはや長大な文字列として固められクライアントに送信される静的なHTML群ではない。ビューは、データにしたがって変化する長寿命のデータ表現形式だ。Batmanのビューシステムはプロパティの能力を強化する。

Alfred(BatmanのToDoアプリサンプル)用のビューの簡易版が以下だ。

<h1>Alfred</h1>
 
<ul id="items">
    <li data-foreach-todo="Todo.all" data-mixin="animation">
        <input type="checkbox" data-bind="todo.isDone" data-event-change="todo.save" />
        <label data-bind="todo.body" data-addclass-done="todo.isDone" data-mixin="editable"></label>
        <a data-event-click="todo.destroy">delete</a>
    </li>
    <li><span data-bind="Todo.all.length"></span> <span data-bind="'item' | pluralize Todo.all.length"></span></li>
</ul>
<form data-formfor-todo="controllers.todos.emptyTodo" data-event-submit="controllers.todos.create">
  <input class="new-item" placeholder="add a todo item" data-bind="todo.body" />
</form>

トランスパイラ層をあきらめ(HAMLなし)、テンプレート層もあきらめた(Ecoもjadeもmustacheもなし)。BatmanのビューシステムはHTML5で、ダウンロードしたその場でブラウザがレンダリングする。JavaScript文字列ではなくバリッドなDOMツリーで、Batmanが解析してデータを埋め込む。コンパイルや文字列処理は行わない。素晴らしいことにBatmanはノードの値をランタイムで監視することで「バインド」する。JavaScriptの世界で値が変化すると、バインドされた対応するノードの属性は自動的に更新され、ユーザはその変化を見る。逆もまた真で、テキストフィールドに入力したり、チェックボックスをクリックしたりすると、文字列や真偽値がJavaScriptのバインドされたオブジェクトにセットされる。CocoaあるいはJavaScriptならKnockoutやSproutcoreにあるようにバインドの概念は新しいものではない。

バインドを選択したのは a)手動でデータの変更をチェックしたくない b)データがすこし変わったぐらいでテンプレート全体を再描画したくない からだ。MustacheやjQuery.tmplに類するシステムでは、おどろくほど頻繁にその両方をする羽目になった。たったひとつのノードを更新したくてあるひとつの要素のひとつのキーを変更しただけで、ループ内で全要素を再描画したりそれらのノード追加のペナルティを支払うのは時間の無駄に思える。SproutCoreのSC.TemplateViewやYehuda Katzの作ったHandlebars.jsはこうした労力を減らすにはよくできているが、やはりブラウザの中で全ての文字列演算をやりたくはないので、ビューの全データを厳密にプロパティにバインドする外科的な精密さの方を選択した。

ビューの通常レベルの複雑さのロジックとひきかえに、ロード中画面のいらない高速な描画を最後には手に入れた。Batmanのビューエンジンには、条件分岐、ループ、コンテクスト、簡単な変換などがあるが、コードを書くことはできない。Batmanでは対話用の複雑なコードはBatman.Viewのサブクラスに記述しなければならず、HTMLレンダリングはそれをもっとも得意するものにやらせる。すなわちブラウザに。

もっと知りたい?
Batmanはこの洒落た深いキーパスの機能や奇妙な「テンプレートじゃないHTML」の他にもいろいろなことができる。擬似ページ間リンクのためのルーティングがある(GETのパラメータまたはセグメント対応)。Batman.Model層はデータの処理や送信を行い、Railsã‚„localStorageのようなストレージバックエンドですぐに使える。Batman.StateMachineã‚„Batman.EventEmitterはオブジェクトにmixinして使う。他にもたくさんある。Webサイト、GitHubにあるソース、[irc://freenode.net/batmanjs:title=freenodeの#batmanjs]などをチェックすることを強くおすすめする。質問、フィードバック、パッチなどは大歓迎だし、Batmanをどう改善したらいいかの提案はいつでも受け入れる。