原文:Basic Middleware Pattern in JavaScript。2020.10.05, by Munif Tanjim
思考过流行的 Web 框架(例如 Express 或 Koa)中间件是如何工作的吗?
在 Express 中,我们这样定义中间件:
const middleware = (req, res, next) => {
// 做事情
next()
}
在 Koa 中,则是这样定义的:
const middleware = (ctx, next) => {
// 做事情
next()
}
基本就是有一些对象(对 Express 来说是 req
、res
,对 Koa 来说是 ctx
),以及一个 next()
函数,作为中间件函数的参数。当调用 next()
时,下一个中间件函数就会被调用。如果修改了当前中间件函数中的参数,那么下一个中间件将接收到这些修改后的参数。例如:
app.use((ctx, next) => {
ctx.name = 'Doe'
next()
})
app.use((ctx, next) => {
console.log(ctx.name) // `Doe`
})
app.use((ctx, next) => {
// 这个中间件不会被调用
})
如果不调用 next()
函数,执行就会在当前的中间件停止,下一个中间件函数将不会被调用。
实现
那么,你如何实现这样的模式?用 30 行 JavaScript 代码就能搞定:
function Pipeline(...middlewares) {
const stack = middlewares
const push = (...middlewares) => {
stack.push(...middlewares)
}
const execute = async (context) => {
let prevIndex = -1
const runner = async (index) => {
if (index === prevIndex) {
throw new Error('next() called multiple times')
}
prevIndex = index
const middleware = stack[index]
if (middleware) {
// 执行当前中间件
// 传入的第二个参数即 `next()`,表示执行下一个中间件
await middleware(context, () => {
return runner(index + 1)
})
}
}
await runner(0)
}
}
这个中间件模式的实现和 Koa 几乎一样。如果你想看看 Koa 是如何做到的,可以查看 koa-compose 包的源代码。
使用
让我们看一个使用它的例子:
// 创建一个中间件 pipeline
const pipeline = Pipeline(
// 传入一个初始中间件
(ctx, next) => {
console.log(ctx)
next()
}
)
// 添加一些中间件
pipeline.push(
(ctx, next) => {
ctx.value = ctx.value + 21
next()
},
(ctx, next) => {
ctx.value = ctx.value * 2
next()
}
)
// 添加终止中间件
pipeline.push((ctx, next) => {
console.log(ctx)
// 不调用 `next()`
})
// 为了增加乐趣,再加一个 ¯\_(ツ)_/¯
pipeline.push((ctx, next) => {
console.log('当然,这个中间件不会被调用,这块消息也不会打印')
})
// 使用初始值 `{ value: 0 }`(即 `ctx` 的值)执行 pipeline
pipeline.execute({ value: 0 })
如果你运行上述代码,能猜出输出会是什么吗?没错,你猜对了:
{ value: 0 }
{ value: 42 }
顺便说一下,这对于异步中间件函数也绝对适用。
TypeScript
让我们为代码库添加一些 TypeScript 吧。
type Next = () => Promise<void> | void
type Middleware<T> = (context: T, next: Next) => Promise<void> | void
type Pipeline<T> = {
push: (...middlewares: Middleware<T>[]) => void
execute: (context: T) => Promise<void>
}
function Pipeline<T>(...middlewares: Middleware<T>[]): Pipeline<T> {
const stack: Middleware<T>[] = middlewares
const push: Pipeline<T>['push'] = (...middlewares) => {
stack.push(...middlewares)
}
const execute: Pipeline<T>['execute'] = async (context) => {
let prevIndex = -1
const runner = async (index: number): Promise<void> => {
if (index === prevIndex) {
throw new Error('next() called multiple times')
}
prevIndex = index
const middleware = stack[index]
if (middleware) {
await middleware(context, () => {
return runner(index + 1)
})
}
}
await runner(0)
}
return { push, execute }
}
现在所有东西都声明了类型,你可以为中间件 pipeline 指定上下文对象类型,像这样:
type Context = {
value: number
}
const pipeline = Pipeline<Context>()
搞定收工!