T3 JavaScript Framework試した

[T3 JavaScript Framework](http://t3js.org/)

DropBox, SkyDrive, Google Driveと競合するクラウド・ストレージサービスのBoxがフロントエンドに使っているJavaScriptフレームワーク。

他のフレームワークと違いフレームワークとして提供される機能は薄いが、テストしやすい疎結合な設計を目指している。その目標としては、バッドパーツを作りにくいようにすることを挙げている。

また、他のフレームワークのようにMVC, MVP, MVVMといった分類での構成を持たないので、Backbone, Reactのような他のフレームワークやライブラリとも協調させられるとしている。

# T3のコンポーネント

T3ではコンポーネントとして以下の３種類を定義している。

* **Modules** 特定のDOMと結びつけられ、表示やイベントの制御を行う
* **Services** DOMとは切り離された再利用可能なツール (Ajax, Cookie, etc.)
* **Behaviors** Modulesをまたいで再利用されるイベント制御のためのmixin

疎結合な状態を保つため、Modules, Behaviorsはお互いに直接操作することを禁じられており、メッセージングにより操作を行う。

# インストール、起動

Browserifyでのビルドは今のところ対応していないので、NPMインストール、ダウンロード、rawgit経由のいずれかでJSファイルを`script`タグから読み込む。イベント制御に別途jQueryが必要。

```bash
$ npm install jquery t3js
```

```html
<script src="./node_modules/jquery/dist/jquery.js"></script>
<script src="./node_modules/t3js/dist/t3.js"></script>
```

T3コンポーネントの管理はグローバルに定義されている`Box.Application`が行う。コンポーネントを管理された状態におくには、コンポーネント定義後に`Box.Application.init()`を呼び出す。呼び出すタイミングはHTML最下段か`DOMContentLoaded`か`onload`あたり。

ちなみに`Box.Application`の存在はBoxのログイン画面でも見ることができる。開発者ツールを立ち上げて`Box.Application`を叩くとオブジェクトが存在していることがわかる。

# Module

```html
<div data-module="module-name">
  <button data-type="push-me-btn">push me</button>
  pushed <span class="pushed-times"></span> time(s).
</div>
```

```js
Box.Application.addModule('module-name', function(context) {
  'use strict';

  var counterEl, counter;

  return {
    init: function() {
      var moduleEl = context.getElement();
      counterEl = moduleEl.querySelector('.pushed-times');
      counter = 0;
      counterEl.innerHTML = counter;
    },

    onclick: function(event, element, elementType) {
      if (elementType == 'push-me-btn') {
        counter++;
        counterEl.innerHTML = counter;
      }
    },

    destroy: function() {
      counterEl = null;
      counter = null;
    }
  };
});
```

DOMに`data-module`を定義すると自動的に該当のModuleへバインドされる。バインディングは用意されていないので、他にバインディングを用意しない場合はHTMLを書いてJavaScriptでDOM操作することで変更を反映していくこととなる。

コンストラクタ、デストラクタが`init`, `destroy`として定義できるので、DOMオブジェクトのキャッシングやModuleが生きている間に参照される変数の初期化はこの時に行う。定義時に対象のDOMをラップした`context`が与えられるので、DOMを取り出して扱うことができる。

特筆すべきは、`data-module`をつけたDOMの領域全てのクリックイベントが`addModule`で定義した`onclick`にdelegateされることである。`onclick`内部で処理を振り分けるためにも、`data-type`要素が必要とされる。これは他のDOMイベントにおいても同じことが言える。

# Service

```js
Box.Application.addService('count-service', function(application) {
  'use strict';

  var counter = 0;

  return {
    countUp: function() {
      counter++;
      return counter;
    }
  };
});
```

ServiceもModuleと同じように`addService()`で定義できるが、DOMに依存しない処理であるから`Box.Application`が引数として与えられる。`Box.Application`から`getService()`で他のServiceは取得できるが、先ほど挙げた原則どおり、Moduleへと直接アクセスすることはできない。

ModuleからServiceを利用する時は、contextから`getService()`を使って名前で呼び出すことができる。

# Module間メッセージング

前述のとおりModule同士が直接操作することは禁止されている。他のModuleに情報を伝達するには、`context.broadcast(key, value)`を使う。broadcastはその名のとおり、すべてのアクティブなModuleへと情報が伝達される。そのため、フィルタリングや処理の振り分けはbroadcastを受信するModule側で実装する。

```js
Box.Application.addModule('module-name', function(context) {
  'use strict';

  return {
    messages: ['message-from-other-module'],

    onmessage: function(key, value) {
      if (key == 'message-from-other-module') {
        // 受信側Moduleでやりたい処理を記述する
      }
    }
  };
});
```

# 所感

Behaviorやテストについては今回省略したが、

* 機能を薄く保つことで、特定の仕様と機能セットにロックインされることを防ぐ
* コンポーネント間の相互作用を`broadcast`メッセージングによって単純化、疎結合化する
* その結果としてコンポーネント単体とメッセージングにのみ注目すればよくなる
* （擬似的なものを含め）グローバル変数への依存を極力排除する

あたりはBoxにおけるJavaScript開発の実践知として裏打ちされているものと思われる。

一方で`Box.Application`というネームスペースをグローバルに掘っているところやCommonJS対応していないところに、わずかながら古さを感じる。

なお[T3の設計思想](http://t3js.org/docs/guides/thinking-in-t3)として「Progressive Enhancementの観点から、サイトを便利にするためのJavaScriptは必須ではない」と言われているので、SPAとしての使用はいろいろ載せないと厳しそう。