介绍
💡 koa-compose 将多个中间件合并成一个大的中间件。调用这个大的中间件就可以依次执行其内多个小中间件,从而完成一系列任务。
正是 koa-compose 将多个中间件组合才形成了 koa 的洋葱模型。
koa 洋葱模型
源码分析
下载 koa-compose 发现其逻辑只存在于 index.js 文件中,文件只有 48 行,导出一个 compose 函数。它接收一个需要合成的中间件数组,返回一个新的中间件。
compose 函数先是对入参 middleware 进行校验,判断其是否为函数数组,如果不是则抛出错误。如果是,通过校验,返回一个函数,这个函数就是新生成的中间件。
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) {
...
}
}
以下为 compose 返回的中间件的具体逻辑:
- 定义
index:middleware[index]为上一次被调用的中间件,由于最开始没有中间件被调用,所以初始化为-1 - 定义
dispatch函数:其入参i,内部将调用middleware[i]中间件 - 执行
dispatch(0),开始逐一调用中间件。
function compose (middleware) {
...
return function (context, next) {
// 1
let index = -1
// 2
function dispatch (i) {
...
}
// 3
return dispatch(0)
}
}
dispatch 函数
dispatch 函数是最精妙的部分,其内部会调用中间件 middleware[i],并通过巧妙入参,使得这个中间件具有调用下一中间件的能力。
其核心逻辑简化为:
function dispatch (i) {
const fn = middleware[i] // eg: fn = loggerMiddleware
fn(context, dispatch.bind(null, i + 1))
}
function loggerMiddleware(context, next) {
// ... -> 调用下一中间件 `前` 的逻辑
next() // 调用下一中间件
// ... -> 调用下一中间件 `后` 的逻辑
}
可以看到 dispatch 在调用 middleware[i] 的时候向其传入的第二个参数 next 为 dispatch.bind(null, i + 1),中间件在调用 next() 时,等同调用 dispatch(i + 1),即调用下一中间件。
dispatch 完整逻辑如下:
-
判断当前中间件是否被执行过。
比较
i和index大小:middleware[i]为本次需要调用的中间件。middleware[index]为上次调用的中间件。
若
i <= index表示已经调用过middleware[i]后面的中间件。中间件是顺序执行的,所以middleware[i]曾经也被调用过。前文知道middleware[i]是dispatch(i)调用的,而dispatch()是开发者通过next()调用,所以抛出错误new Error('next() called multiple times')若当前中间件
middleware[i]是第一次调用,则通过校验,同时更新index = i -
确定要调用的中间件
fn。正常情况下
fn = middleware[i],但还记得前文说开发者写的中间件逻辑中会调用next()来调用下一中间件吗?若当前中间件是middleware数组中最后一个中间件怎么办?(最后一个中间件是通过dispatch(middleware.length - 1)来调用,其内部调用next()时,相当于调用dispatch(middleware.length))if (i === middleware.length) fn = next就是来应对这个 case 的,这时fn被设置为next,即compose返回中间件的第二个入参。(其实还有i === middleware.length + 1的情况,读者可以思考一下~) -
调用中间件
fn。如果
fn不存在(next === undefined || i === middleware.length + 1),则返回空的 resolve 对象。如果
fn存在,则调用fn,并入参next = dispatch.bind(null, i + 1)。
function compose (middleware) {
...
return function (context, next) {
let index = -1
function dispatch (i) {
// 1
if (i <= index) return Promise.reject(new Error('next() called multiple times'))
index = i
// 2
let fn = middleware[i]
if (i === middleware.length) fn = next
// 3
if (!fn) return Promise.resolve()
try {
return Promise.resolve(fn(context, dispatch.bind(null, i + 1)))
} catch (err) {
return Promise.reject(err)
}
}
return dispatch(0)
}
}
总结
koa-compose 利用函数式编程 compose 的思想,接受一个中间件数组,返回一个新的中间件。其代码很短,除去注释信息只有 23 行,设计也很精妙。它使得被合成的中间件通过调用 next(),从而对下一个中间件进行调用,而不用考虑多个中间件是如何连接在一起的。建议读者自己动手翻翻源码。可以对函数式编程 compose 思想和闭包有更清晰的认识。