読者です 読者をやめる 読者になる 読者になる

15 min/d

ぼうずやのにっき

koajs/koa をためした

koa (koajs/koa) を試している。

経緯としては、次の 3 つの衝突地点だからだ。

  • 2016-06-19 の今週の目標「beater を利用した App を 1 つつくる」を達成したい
  • 2016-06-21 の generator 学習の成果を活かしたい
  • bouzuya/beater の generator 対応を進めたい

generator を使う web application framework で最初に思い浮かんだのが koa だった。

koa の API はそこまで多くない。基本は app.use() つまり middleware の解釈と挙動を把握すれば大丈夫だ。Source Code の量もそこまで多くないのでざっと読む (外部 npm package は多いが core となる箇所は少ない) 。結論から言うと、次の記事の最後に示された実装が分かりやすい。

Koa(1.x)のmiddlewareの仕組みを調べた - Qiita

直感的な yield next; による midleware の挙動は co による generator の入れ子の解決に依存している。

例えば、次のように middlware を設定する。

app.use(mw1);
app.use(mw2);

koa-compose によりこれらは mw1(mw2(noop())) のように呼び出される。この形から app.use(function* (next) { yield next; });next が何者か分かる。この next は処理の実行順序で見たときに次の middleware の返した generator (IterableIterator<T>) だ。

これは期待する処理の実行順序と「逆順」に generator function を呼び出していることを意味する。これは問題にならない。なぜなら generator function は呼び出した際に generator を返すだけで処理を実行するわけではないからだ。

処理の実行順序の最初にある generator function 、つまり最後の generator function は入れ子になった generator を返す。これは IterableIterator<IterableIterator<IterableIterator ...>> のような構造だ。この構造は middleware を追加すればするほど階層が深くなる。実際にはそれが何層になっているかは問題ではない。

この構造を co.wrap で上記の入れ子になった generator を Promise を返す関数に変換する。最後にその関数を、 Koa.Contextthis として呼び出せば終わりだ。 co はこの入れ子になった generator を期待する処理の実行順序どおりに実行してくれる。

基本的な部分はこれで十分だろう。

最初 DefinitelyTyped/DefinitelyTyped から取得した .d.ts が koa (2.x) のものだと気づかず、 document との食い違いに混乱した。2.x は this の代わりに引数で Koa.Context を渡すようで良いのだけど、async を使っているので、まだ対応しないことにした。