世の中では、VueやReactなどのイケイケドンドンFFWが大人気だが、それ以外にも様々なフロントエンド・フレームワークがひっそりと息をしている。ということで、今回はChoo.jsという4kbの小さなフレームワークの紹介をしようと思う。
Choo.jsとは
GithubのREADMEにA 4kb framework for creating sturdy frontend applications
と書いてあるように、Choo.jsは小さく始めながらも堅牢なフロントエンド・アプリケーションを作れるような機能を提供するフレームワークだ。
Choo.js自体はたった200行程度のソースコードから成り立っていて、アプリケーションが提供しているルーティングの機能や、EventEmitterなどは、nanorouterやnanobusなどのいくつかの異なる小さなモジュールの組み合わせから成り立っている。Choo.jsのエコシステムの一部として、これらのモジュールはFramework-agnostic1であるということを主眼に置いており、できるだけ粗結合で依存の少ないコードベースであるということを謳っている。
使ってみる
チュートリアルということでWebpackBinをここでは使う。
以下のURLをブラウザから開けば実際に動かしたりソースコードをいじったりできる。
https://www.webpackbin.com/bins/-L-V4Vk0BYQ12x5CR7Fs
HTML
HTMLは以下の数十行だけでよい。メインの実装はJS側で行う。
<!doctype html>
<html>
<head>
<meta charset="utf-8"/>
</head>
<body>
<div id="app"></div>
<script src="main.js"></script>
</body>
</html>
JS
JS側で実際にChooを使ったアプリケーションの構築を行っていく
Choo.jsにはビューとストアという2つの概念しか無いと行っても、ほぼほぼ差し支えない。
ビュー関数はES6のタグテンプレート・リテラルを用いてDOMを構造を返すだけの純粋な関数だ。その定義の中でのボタンのクリックやなんらかの変更を、引数として受け取っているemit
関数に渡して呼び出すことで、ストアが変更をキャッチできるようになる。
//
// ◎ビュー定義関数
// state(ステート)を受け取り、DOMを返す関数としてビューを定義
//
function mainView (state, emit) {
// ES6のタグテンプレート・リテラルでDOM構造を定義できる
return html`
<div>
<h1>Title: ${state.title}</h1>
<input type="text" value="${state.title}" oninput=${update} />
</div>
`
// 引数から受け取っているemit関数を呼び出すことでストアの更新をトリガできる
function update(e) {
emit('update', e.target.value)
}
}
ストアはEventEmitterのインスタンスとしてemitter
を受け取るので、それに対してリスナを登録してやるだけでよい。render
をemitすることでビューにステートを反映できる。
また、自前でイベントを登録する以外にも、DOMがマウントされたときに呼び出される'DOMContentLoaded'
などのライフサイクル・ハンドラも用意されている2
//
// @ストア定義関数
// 引数で受け取るstateとemitterに対してストアとイベントハンドラを定義する
//
function titleStore (state, emitter) {
// stateに属性を生やす形でデフォルト・ステートを定義
state.title = 'Not quite set yet'
// ビュー関数からemitでトリガされるイベントハンドラを定義
emitter.on('update', function (title) {
state.title = title
// ビューの更新を実行
emitter.emit('render')
})
}
これらの一連の流れをひとつにまとめると、このような感じになる。
const html = require("choo/html");
const choo = require("choo");
const app = choo();
function mainView (state, emit) {
return html`
<div>
<h1>Title: ${state.title}</h1>
<input type="text" value="${state.title}" oninput=${update} />
</div>
`
function update (e) {
emit('update', e.target.value)
}
}
function titleStore (state, emitter) {
state.title = 'Not quite set yet'
emitter.on('update', function (newTitle) {
state.title = newTitle
emitter.emit('render')
})
}
app.use(titleStore)
app.route('/', mainView)
app.mount('#app')
余談: ミドルウェア
上のコードではストアをapp.use()
を使って登録しているが、厳密にはchoo.jsのストアはミドルウェアのひとつとして作られているに過ぎない。Choo.jsでは、state
とemitter
を受け取る関数であれば、なんでもuseメソッドを使って登録することができる。
function YourMiddleware() {
return function (state, emitter) {
// do your work here
}
}
app.use(YourMiddleware());
公式で容易されているchoo-devtoolsなどのモジュールも同じような形で利用できるようになっている
const choo = require('choo');
const devtools = require('choo-devtools');
const app = choo()
if (process.env.NODE_ENV !== 'production') {
app.use(devtools())
}
app.mount('body')
choo-cliを使う
そもそもフロントエンドは開発環境を構築するのがダルい。Webpackの設定ファイルなど書きたくない。そんな人のためにchoo-cliというスキャフォルディングツールが用意されているので、それを使って新しくプロジェクトを生成することもできる。
$ choo new example-choo-app
$ cd example-choo-app
choo-cliで生成されるアプリケーションにはデフォルトでbankai3という、フロントエンドアプリケーションのバンドリングから開発用サーバの起動までをワンストップでやってくれる便利なツールが追加され、以下のようなスクリプトが使えるようになる
$ npm start
$ npm run build
まとめ
個人的によさそうだなと思ったところは以下の3つ
コードベースが小さく、困ってもソースを読めばなんとかなる
- フレームワークの実装があまりにも巨大すぎると、issueやStackoverflowにないような困りごとを自分でソースコードを読んで解決するのはなかなか難しかったり、解決できてもPRの粒度を小さく保つのが難しくなったりする。加えて、OSSなソースコードリーディングにも最適。
ビューが基本的に関数というアプローチ
- Choo.jsにおいて、ビューはクラスのインスタンスでもなんでもなく、単なるDOM構造を返すだけの関数でしか無い。つまり常にステートレスで、責務が明確に分離されている。純粋な関数として実装されるのでテストも容易な上、DIされるのはEventEmitterだけなのでモックも簡単。
最低限のEventEmitterだけが用意されている
- Choo.jsにはビューとストアという2つのレイヤしか用意されていない。そして、ストアは単なるEventEmitterでしかない。ここに自前でFluxアーキテクチャを実装してもいいし、これをこのままつかってもよい。データ・フローの実装への自由度が高い。
ぜひ気になったら触ってみてください
-
特定のフレームワークに依存しないこと ↩