◆ ctx が条件を満たしたときのみ特定のミドルウェアを通す
◆ 特定のミドルウェアでのみ ctx を偽る
◆ ミドルウェアの動作確認

Koa のミドルウェアはわかると便利なのですが デフォルトの機能では不便なところもあります
というか たいていはミドルウェア側が作りこまれてないというか考慮してないものが多いです
ほとんどが非公式かつ hapi みたいに企業がやってるわけでもないので PR も放置されてたりと積極的に更新されないものが多いです
直接コード書き換えれば簡単に対処できるのにと思うものが多いですが それはそれで面倒事も増えるので気が進まないです

ものによってはハードコーディングされていたりで どうしようもないのですが ミドルウェアの仕組みを使った工夫でどうにかできるものもあります
そういうものをまとめてみました
探せば npm ライブラリにありそうな感じではありますが コード自体は短くわざわざパッケージを入れるほどでもないものです

filter

特定のパスの場合など条件を満たしたときだけ 特定のミドルウェアを実行したいということはあります
router に近いですが router とはまた異なるものです
例えば static で特定のフォルダやファイルは特別扱いしたいので除外したいということはありますが exclude みたいな機能はなく static では対応していません
そういうときに条件を見たした場合のみにミドルウェアを通すことで解決できます

const filter = (cond, mid) => {
return (ctx, next) => {
if (cond(ctx)) {
return mid(ctx, next)
} else {
return next()
}
}
}

使用例はこういうのです

app.use(
filter(
ctx => {
return ctx.path.startsWith("/foo") && !ctx.path.startsWith("/foo/bar")
},
middleware()
)
)

/foo の中で /foo/bar の中ではない場合のみ middleware() で作ったミドルウェアを通します
filter 処理で受け取るのは ctx なので path に限らず request の情報も使えます

branch

上の filter は条件にマッチしたときのみでマッチしなかったときはありません
else if や else に当たるものを使いたいときはこの branch です

const branch = map => {
return (ctx, next) => {
for (const [cond, mid] of map) {
if (cond(ctx)) {
return mid(ctx, next)
}
}
return next()
}
}

使い方は filter の引数のペアを key, value にした Map を引数に渡します
new Map をせずに ただの二重配列でも大丈夫です

app.use(
branch(
[
[
(ctx) => { ... },
middleware1(),
],
[
condition2,
middleware2(),
],
[
(ctx) => true,
middleware3(),
]
]
)
)

ミドルウェアの仕組み上 条件にマッチした場合に通すミドルウェアの next でも マッチしなかったときの next でも 次に app.use で登録したミドルウェアが呼び出されます
完全に分岐して 独立させたいならこれを app.use の最後に登録する必要があります

条件に対して複数のミドルウェアを組み合わせたいなら compose を使えば複数のミドルウェアを 1 つのミドルウェアにまとめられます

fake

使いたいミドルウェアが特定の ctx のプロパティを使用するということはよくあります
そういうときに「このミドルウェアでだけプロパティを別のものに置き換えたい」と思うことがあります
直前のミドルウェアで ctx のプロパティを書き換えればできはしますが その場合はそれより下流のミドルウェア全てに影響してしまいます
1 つのミドルウェアだけでプロパティを別の値にしたいです

また ctx にプロパティを追加するミドルウェアもあります
別のものと競合するなどの理由で追加してほしくなかったり デフォルトとは別の名前にしてほしいことがあります

これらに対応するために ctx を偽装するのが fake です

const fake = (mid, before, after) => {
return async (ctx, next) => {
const _ctx = Object.create(ctx)

await before?.(_ctx, ctx)
await mid(_ctx, next)
await after?.(_ctx, ctx)
}
}

代わりに ctx として渡す値は完全に新規のオブジェクトよりも ctx を少しだけ変えたものという事が多いと思うので プロトタイプチェーンで ctx とつないでいます
ミドルウェア呼び出しの前後の処理を書くことができて before では そのミドルウェア用に ctx のプロパティの書き換え after では そのミドルウェアが偽物の ctx に追加したプロパティを本来の ctx に追加する処理を行います

使用例はこんな感じです

const middleware = () => {
return async (ctx, next) => {
console.log("M vvvvv:", ctx.foo)
ctx.foo = ctx.foo + "1"
await next()
ctx.foo = ctx.foo + "2"
console.log("M ^^^^^:", ctx.foo)
}
}

app.context.foo = "default"

app.use(async (ctx, next) => {
console.log("1 vvvvv:", ctx.foo)
await next()
console.log("1 ^^^^^:", ctx.foo)
})

app.use(
fake(
middleware(),
ctx => {
ctx.foo = "fake"
console.log("before middleware:", ctx.foo)
},
ctx => {
console.log("after middleware:", ctx.foo)
},
)
)

app.use(async (ctx, next) => {
console.log("2 -----:", ctx.foo)
})
1 vvvvv: default
before middleware: fake
M vvvvv: fake
2 -----: default
M ^^^^^: fake12
after middleware: fake12
1 ^^^^^: default

ctx.foo を書き換えます
初期値は default という文字列にしています
1 のミドルウェアでは default と表示されています
before の関数で ctx.foo を fake に書き換えると middleware() で作ったミドルウェア内では fake として受け取れています
そのミドルウェアの中では ctx.foo に 1 を追加して fake1 にして次のミドルウェアを呼び出しています
しかし 2 のミドルウェアでは default を受け取っています
middleware() の関数に戻ってきたときに 2 を追加した結果 fake1 が残っているので fake12 になります
after の関数では ctx.foo は fake12 になっています
そしてひとつ前の 1 のミドルウェアに戻ったときには fake12 は残っていなくて ctx.foo は default になっています

after 関数では 2 つめの引数に本来の ctx を受け取るので変更を維持するには 自分で代入します

(fctx, ctx) => {
ctx.foo = fctx.foo
},

ミドルウェアの動きの確認

ミドルウェアの動きってシンプルなようで難しいので こういうツールを作ってるとちゃんと動くのか確認したいことが多いです
かと言って Koa を入れて Web サーバを起動してリクエスト送ってというのでは面倒が多いです

そういうときに簡単にミドルウェアの処理の流れを確認するには koa-compose が使えます
実際に Koa が app.use したミドルウェアを処理しているものです

const compose = require("koa-compose")

const mid = compose([
async (ctx, next) => {
console.log("1-before")
await next()
console.log("1-after")
},
async (ctx, next) => {
console.log("2-before")
await next()
console.log("2-after")
},
async (ctx, next) => {
console.log("3")
},
])

const ctx = { x: 1 }
mid(ctx).then(console.log, console.err)
1-before
2-before
3
2-after
1-after

ミドルウェアの配列を渡して返ってきた関数を呼び出します
引数には ctx とするオブジェクトを入れます
デバッグ実行もやりやすいです

上の filter 関数を試してみるとこんな感じです

const compose = require("koa-compose")

const filter = (cond, mid) => {
return (ctx, next) => {
if (cond(ctx)) {
return mid(ctx, next)
} else {
return next()
}
}
}

const mid = compose([
filter(
ctx => ctx.x,
(ctx, next) => {
console.log("filter-middleware")
return next()
}
),
async (ctx, next) => {
console.log("next")
},
])

!async function() {
console.log("### x:true")
await mid({ x: true }).then(console.log, console.err)
console.log("### x:false")
await mid({ x: false }).then(console.log, console.err)
}()
### x:true
filter-middleware
next

### x:false
next

自作のものを試すときにはいいのですが 実際のライブラリのミドルウェアも入ってくると ctx から参照するものが増えてダミーの値を準備するのが面倒になり Koa で実行したほうが早い場合もあります