DropBox, SkyDrive, Google Driveと競合するクラウド・ストレージサービスのBoxがフロントエンドに使っているJavaScriptフレームワーク。
他のフレームワークと違いフレームワークとして提供される機能は薄いが、テストしやすい疎結合な設計を目指している。その目標としては、バッドパーツを作りにくいようにすることを挙げている。
また、他のフレームワークのようにMVC, MVP, MVVMといった分類での構成を持たないので、Backbone, Reactのような他のフレームワークやライブラリとも協調させられるとしている。
T3のコンポーネント
T3ではコンポーネントとして以下の3種類を定義している。
- Modules 特定のDOMと結びつけられ、表示やイベントの制御を行う
- Services DOMとは切り離された再利用可能なツール (Ajax, Cookie, etc.)
- Behaviors Modulesをまたいで再利用されるイベント制御のためのmixin
疎結合な状態を保つため、Modules, Behaviorsはお互いに直接操作することを禁じられており、メッセージングにより操作を行う。
インストール、起動
Browserifyでのビルドは今のところ対応していないので、NPMインストール、ダウンロード、rawgit経由のいずれかでJSファイルをscript
タグから読み込む。イベント制御に別途jQueryが必要。
$ npm install jquery t3js
<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
<div data-module="module-name">
<button data-type="push-me-btn">push me</button>
pushed <span class="pushed-times"></span> time(s).
</div>
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
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側で実装する。
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の設計思想として「Progressive Enhancementの観点から、サイトを便利にするためのJavaScriptは必須ではない」と言われているので、SPAとしての使用はいろいろ載せないと厳しそう。