Koa について
Nodeには Koa というフレームワークがあります。
人気の度合いについては Deno に Koaに触発されたプロジェクトOakが立ち上がるぐらいには人気があるようです。
ただし、日本語の記事は 2016-2017の v2のリリース以降は言及された記事がそれほど無く人気が盛り上がっている感じはありません。
Koaの特徴
ざっくり
全部入りのフレームワークとは異なり、リクエストやレスポンスの処理の限定的な処理しか持ちません。ちなみにルーティングすら用意されていません。
アプリケーションを拡張する方法は基本的にミドルウェアと呼ばれる関数を定義する方法しかなく、ある意味覚える事が少ないです。
コードを読んでいく上では高階関数の読み慣れが必要な印象ですが、全部入りのフレームワークに比べ、アプリケーションを拡張する際に 基底クラスやインターフェイスの理解(あるいは罠の熟知)が不要な点は気に入っています。
3つポイントを抑える
本体は覚える事が少ないです、以下の3つが主要な概念です。
- context
- middleware
- next
context について
new koa() で新しいアプリケーションを作成した場合、そのアプリケーションの独立した環境(=コンテキスト)オブジェクトが作成されます。
このコンテキストオブジェクトはNodeのリクエストとレスポンスがうまくラップされています。また、状況に応じて拡張することも可能です。
※ グローバルな状態が管理されているオブジェクトとして理解すれば大体あっていそうです。
単純なWebサーバの例です。
const Koa = require("koa");
const app = new Koa();
app.use((ctx /*<- これがコンテキスト*/) => {
//この関数をミドルウェアと呼ぶ
ctx.body = "koa app.";
})
app.listen(3000);
このコンテキストを経由して、リクエストの内容を取得したり、クライアントに情報を渡したりする事が出来ます。
middleware について
ミドルウェアは context を 第一引数、nextを第2引数に持つ関数です。ただそれだけです。
そのため、関数中で何をするか何をするべきで無いかは実装者に委ねられます。
contextの拡張(機能の追加)を行う際もこのミドルウェアを利用します。
Koa はそのミドルウェアを重ねて処理をすると良く説明されますが、複数の関数をうまく合成(compose)する仕組みと言い換えることも出来ます。
単純化してミドルウェアの仕組みを説明します。
const ctx = {state:""}
// ものすごく単純化したミドルウェアを重ねるイメージ
const middlewares = [
(ctx,next) => {
ctx.state = ctx.state + "1";
next();
},
(ctx,next) => {
ctx.state = ctx.state + "2";
next();
},
(ctx,next) => {
ctx.state = ctx.state + "3";
next();
}
]
const dispatch = (i) => {
if(middlewares[i+1]){
middlewares[i]( ctx , () => dispatch(i+1) );
return;
}
middlewares[i](ctx,() => {});
}
dispatch(0);
console.log(ctx.state);
//123
サンプルでは ctx.state に3つのミドルウェアで1文字ずつ足していき最終的に123の文字列が出力されます。 dispatch は nextを通じて 再帰的に処理されます。
つまり next をコールすることで、次に登録されたミドルウェアに処理を移します。
このようにミドルウェアの層を重ねていく事でアプリケーションを開発します。
※シェルでテキストをコマンドとパイプで処理するイメージに近いと思えば近い。
※ 実際にはayncに対応しているため もう少し複雑です。この部分は koa-compose というライブラリが担っています。
next について
前項で説明した通り、nextは次のミドルウェアを呼び出し、後続のミドルウェアでnextが呼ばれる限り再帰的に処理を行います。つまり ミドルウェア中ではnextが呼び出されたあとは自身よりあとのミドルウェアが実行済(解決済)の状態で自身に制御が戻ってきます。
これを公式では ダウンストリーム/アップストリームと表現しているようです。
※ コードではnextより上がダウンストリームでnextより下がアップストリーム。(合ってるか不安
先程の疑似ミドルウェアにコードを追加します。
const ctx = {state:""} // アプリケーションの環境
// ものすごく単純化したミドルウェアのイメージ
const middlewares = [
(ctx,next) => {
ctx.state = ctx.state + "1";
console.log( "pre next" , ctx.state); // 1 が出力される
next();
console.log( "after next" ,ctx.state); // 123 が出力される
// つまり 自分よりあとの2つのミドルウェアが実行された後 この関数に制御が戻ってきている
},
(ctx,next) => {
ctx.state = ctx.state + "2";
next();
},
(ctx,next) => {
ctx.state = ctx.state + "3";
next();
}
];
const dispatch = (i) => {
if(middlewares[i+1]){
middlewares[i]( ctx , () => dispatch(i+1) );
return;
}
middlewares[i](ctx,() => {});
}
dispatch(0);
上の仕組みにより、nextは単純に次に処理を移す事だけでは無くミドルウェアで表現できることの幅を広げてくれる事が理解できるかと思います。
私の脳ではこれを最大限活用するとコード追い辛いように思うんですが
実行される順番がわかりやすいように、もう少し書き加えています。
const ctx = {state:""} // アプリケーションの環境
let i = 1
// ものすごく単純化したミドルウェアのイメージ
const middlewares = [
(ctx,next) => {
ctx.state = ctx.state + "1";
console.log( "down mw#1" , i++ ); //1
next();
console.log( "up mw#1" , i++ ); //6
},
(ctx,next) => {
ctx.state = ctx.state + "2";
console.log( "down mw#2" , i++ ); //2
next();
console.log( "up mw#2" , i++ ); //5
},
(ctx,next) => {
ctx.state = ctx.state + "3";
console.log( "down mw#3" , i++ ); //3
//next();
console.log( "up mw#3" , i++ ); //4
}
]
const dispatch = (i) => {
if(middlewares[i+1]){
middlewares[i]( ctx , () => dispatch(i+1) )
return
}
middlewares[i](ctx,() => {})
}
dispatch(0)
/**
down mw#1 1
down mw#2 2
down mw#3 3
up mw#3 4
up mw#2 5
up mw#1 6
**/
実行順を確認してみると、上から処理が降りていき終端に達すると、今度は上方向に処理が移っていくのがわかりやすくなると思います。
まとめ
ほとんど、ミドルウェアの説明になってしまった。
koa API ドキュメント(3つしかない