关于中间件

使用 node 构建 web 应用时,并不单单响应一个简单的 hello world,在一个实际的业务中,我们也许会做这些:

  • 请求方法的判断。
  • URL 的路径解析。
  • URL 中查询字符串解析。
  • Cookie 的解析。
  • Basic 认证。
  • 表单数据的解析。
  • 任意格式文件的上传处理。

这样一个完整的项目中需要处理很多的细节,当然你也可以都写在一起,但这样代码的耦合程度太高了,而且以后维护起来也令人头大。

为此引入**中间件(middleware)**来简化和隔离这些基础设施与业务逻辑之间的细节,让开发者能够关注在业务的开发上,以达到提升开发效率的目的。

理解中间件的最简单的方式是实现一个基础的中间件模式,一个中间件其实就是一个函数。

一个简单的中间件模式需要一个 use 方法来进行中间件的注册,需要一个 run 来执行这些注册的中间件

const app = {
fns: [],
callback(ctx) {
console.log(ctx)
},
use(fn) {
this.fns.push(fn)
},
run(ctx) {
let index = 0
const next = () => {
index++
}
this.fns.forEach((fn, idx) => {
if (index === idx) fn(ctx, next)
})
index === this.fns.length && this.callback(ctx)
},
}

使用一下:

app.use((ctx, next) => {
ctx.name = "ranxiu"
next()
})

app.use((ctx, next) => {
ctx.gender = "girl"
next()
})

app.run({})
// 打印:{name:"ranxiu",gender:"girl"}

关于 run 函数还有更加优雅的写法:

function run(ctx, stack) {
const next = () => {
const middleware = stack.shift()
if (middleware) {
middleware(ctx, next) // 递归调用
}
}
next()
}

再来看看 koa-compose 的中间件:

function compose(middleware) {
// 提前判断中间件类型,防止后续错误
if (!Array.isArray(middleware))
throw new TypeError("Middleware stack must be an array!")
for (const fn of middleware) {
// 中间件必须为函数类型
if (typeof fn !== "function")
throw new TypeError("Middleware must be composed of functions!")
}
return function (context, next) {
// 采用闭包将索引缓存,来实现调用计数
let index = -1
return dispatch(0)
function dispatch(i) {
// 防止next()方法重复调用
if (i <= index)
return Promise.reject(new Error("next() called multiple times"))
index = i
let fn = middleware[i]
if (i === middleware.length) fn = next
if (!fn) return Promise.resolve()
try {
// 包装next()返回值为Promise对象
return Promise.resolve(fn(context, dispatch.bind(null, i + 1)))
} catch (err) {
// 异常处理
return Promise.reject(err)
}
}
}
}

两个字:优雅。有时不得不感慨人和人的差距有时比人和狗的差距还大。

拿这个 🌰 来说:

function wait(ms) {
return new Promise((resolve) => setTimeout(resolve, ms || 1))
}

const arr = []
const stack = []

// type Middleware<T> = (context: T, next: Koa.Next) => any;
stack.push(async (context, next) => {
arr.push(1)
await wait(1)
await next()
await wait(1)
arr.push(6)
})

stack.push(async (context, next) => {
arr.push(2)
await wait(1)
await next()
await wait(1)
arr.push(5)
})

stack.push(async (context, next) => {
arr.push(3)
await wait(1)
await next()
await wait(1)
arr.push(4)
})

await compose(stack)({})
// arr = [1,2,3,4,5,6]

当 i 为 3 时,

let fn = middleware[i] //fn=undefined
if (i === middleware.length) fn = next
if (!fn) return Promise.resolve() //!fn为true

直接返回 resolve,之后就执行 next()后面的函数

stack.push(async (context, next) => {
arr.push(3)
await wait(1)
await next()
await wait(1)
arr.push(4)
})

执行完后返回第二个 next() 后面继续往下执行,知道所有的中间件执行完毕。

这便是众人皆知的“洋葱模型”。你也可以选择只添加前置的处理,就是 await next()前面的操作

,或者后面的处理。

每个中间件足够的小而美,职责单一,同时多个中间件又具备良好的逻辑拓展性和可组合性,并且易于测试。这个设计模式真是太“漂亮”了。