我正在参与掘金会员专属活动-源码共读第一期,点击参与。
前言
在日常的 Web 开发中,无论是使用 Node.js
进行后端开发, 还是实现 BFF 中间层,或是进行端到端的测试,都可能会出现 koa
的身影。koa
以中间件函数的使用方式而著名,又称洋葱模型,本文就是要将这个洋葱一层层地拨开,探究一下这里面的奥妙之处。
中间件函数的源码地址 — koa-compose。
回顾中间件的使用方式
在分析源码之前,我们先来回顾一些中间件的使用方式。
在这里,我引用了 koa
官方文档上的例子。
在这个用例中,我们可以清楚地看到 await next()
的作用是调用下一个中间件函数。
其实上述的执行过程,可以类比于算法中的递归回溯思想。
源码解析
如果你对上述的用例感到陌生或者不理解,没关系,我们再来看看 koa-compose
的用例:
const compose = require('koa-compose')
const arr = []
const stack = []
stack.push(async (context, next) => {
arr.push(1)
await next()
arr.push(6)
})
stack.push(async (context, next) => {
arr.push(2)
await next()
arr.push(5)
})
stack.push(async (context, next) => {
arr.push(3)
await next()
arr.push(4)
})
await compose(stack)({})
console.log(arr) // [1, 2, 3, 4, 5, 6]
该用例参考了 koa-compose
中测试文件的第一个测试用例,你可以在任意支持 CommmonJS
模块化的文件中运行上述的代码,整个代码的执行顺序就和 arr.push()
中传入的数字一样。
通过 await compose(stack)({})
这行代码,我们可以了解到源码中的两个信息:
-
koa-compose
导出的是一个函数 —compose
,向该函数传入一个数组,数组中的每一个元素都是函数。 -
compose
函数返回的也是一个函数,该函数传入的是一个对象,返回值是Promise
对象。
基于以上的信息,我们已经可以看懂以下源码的逻辑:
function compose (middleware) {
// 判断 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) {
// ... 省略部分代码
return dispatch(0)
function dispatch (i) {
// ... 省略部分代码
try {
return Promise.resolve(/*...省略部分代码*/)
} catch (err) {
return Promise.reject(err)
}
}
}
}
middleware
就是 stack
,context
参数的值为 {}
,next
参数的值为 undefined
,因为 await compose(stack)({})
,没有传入第二个参数值。
接下来,我们就需要思考一个问题:怎么执行所有的中间件函数呢?也就是如何执行参数 middleware
数组中的所有元素?
源码里使用了函数的 bind
方法的返回值,向中间件函数传递 next
参数值:
function compose (middleware) {
// 判断 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) {
// ... 省略部分代码
return dispatch(0)
function dispatch (i) {
// ... 省略部分代码
let fn = middleware[i]
// 执行了最后一个函数后,i 的下一次值为 middleware.length
if (i === middleware.length) fn = next
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)
表示开始执行第一个中间件函数,也意味着await compose(stack)({})
返回的是第一个中间件函数的返回值,而不是最后一个中间件函数的返回值。 -
此时
let fn = middleware[i]
取到的就是第一个中间件函数。 -
代码执行到
return Promise.resolve(fn(context, dispatch.bind(null, i + 1)))
,执行第一个中间件函数,还记得每个中间件函数中都有context
和next
参数吧?next
参数值就是dispatch.bind(null, i + 1)
,表示将下一个(第二个)中间件函数的执行传递给next
参数,最后将第一个中间件函数的返回值传入给Promise.resolve()
方法。所以说对于第一个中间件函数而言,内部的await next()
语句就等于await dispatch(1)
。
stack.push(async (context, next) => {
arr.push(1)
await next() // await dispatch(1)
arr.push(6)
})
-
代码执行到
await next()
后,会重复执行第二个和第三个步骤,let fn = middleware[i]
取到的就是第二个中间件函数,return Promise.resolve(fn(context, dispatch.bind(null, i + 1)))
执行第二个中间件函数,其中dispatch.bind(null, i + 1)
表示将第三个中间函数的执行传递给next
参数,所以在第二个中间函数中的await next()
等于await dispatch(2)
。 -
如此重复执行第二个和第三个步骤,直到执行最后一个(第三个)中间件函数,它的内部仍然有
await next()
(await dispatch(3)
)语句,这是为什么呢?按照常规流程来说,这里执行到最后一个函数,应该不需要再往下执行下一个函数了。其实,最后的await next()
语句是用来执行compose
函数内部的next
函数,上文有一处也提到过,它是可以通过await compose(stack)({})
来传递的,即:
await compose(stack)({}, async (context, next) => {
// ...
})
- 那怎么去同时兼容
await compose(stack)({})
没传递next
函数的情况呢?下面这两行代码就解决了这个问题:
// ... 省略部分代码
if (i === middleware.length) fn = next
if (!fn) return Promise.resolve()
// ... 省略部分代码
代码执行到了 await dispatch(3)
,那么 let fn = middleware[i]
取到的值肯定是 undefined
,同时,i
的值肯定也等于 middleware.length
,这时就需要将 next
函数赋值给 fn
,如果最后还是为 undefined
,那就停止继续执行,返回状态为 fulfilled
的 Promise
对象;否则继续执行,重复第二个和第三个步骤,最后会再次执行到 if (!fn) return Promise.resolve()
语句,停止继续执行。
到此为止,我们已经掌握了 90% 的中间件源码了。剩下 10% 的源码是用来解决在一个中间件函数内多次执行 await next()
语句的问题。比如,在第一个中间件函数内执行两次 await next
语句:
stack.push(async (context, next) => {
arr.push(1)
await next()
await next()
arr.push(6)
})
这时候,就需要在第二次执行 await next()
语句的时候抛出错误。
源码中的实现方式是:通过一个 index
变量,来记录上一次执行中间件函数的索引,如果 index
变量值大于或等于当前将要执行中间件函数的索引,就说明是多次执行了 await next()
,需要抛出错误。
function compose (middleware) {
// 判断 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) {
// 记录上一次执行中间件函数的索引,初始值为 -1。
let index = -1
return dispatch(0)
function dispatch (i) {
// 说明在同一个中间件函数多次执行了 await next()
if (i <= index) return Promise.reject(new Error('next() called multiple times'))
index = i
let fn = middleware[i]
// 执行了最后一个函数后,i 的下一次值为 middleware.length
if (i === middleware.length) fn = next
if (!fn) return Promise.resolve()
try {
return Promise.resolve(fn(context, dispatch.bind(null, i + 1)))
} catch (err) {
return Promise.reject(err)
}
}
}
}
为什么可以这么判断呢?我们依然是拿上面三个中间件函数作为例子:
stack.push(async (context, next) => {
arr.push(1)
await next() // await dispatch(1)
await next() // await dispatch(1)
arr.push(6)
})
stack.push(async (context, next) => {
arr.push(2)
await next() // await dispatch(2)
arr.push(5)
})
stack.push(async (context, next) => {
arr.push(3)
await next() // await dispatch(3)
arr.push(4)
})
根据前面已经解析过的执行流程,我们可以得出,在执行第一个中间件函数中的第二次 await next()
语句中,dispatch(1)
函数的 index
值为 3,而此时的 i
值为 1,所以符合错误条件,抛出错误。
其实,我们再细想一下,无论第二次执行的 await next()
语句是在哪个中间件函数,都会先执行完每个中间件函数的第一次 awai next()
语句,在这个例子的执行流程就是:
- 第三行的
await next()
- 第十行的
await next()
- 第十六行的
await next()
- 第四行的
await next()
也就是说,在执行第二次 await next()
语句的时候,index
值一定是 middleware.length
,而每个 await next()
语句中的 i
值都是小于 middleware.length
,所以可以通过 index
值大于或等于 i
值来判断是多次执行了 await next()
,index
等于 i
的情况是在最后一个中间件函数多次执行 await next()
。
好了,以上就是中间件源码的全部,虽然整个源码不到 30 行,但它的逻辑性比较强,思维缜密,还是值得我们反复斟酌学习的。
总结
- 中间件函数中的
await next()
的作用是调用下一个中间件函数。 - 每个中间件函数都需要执行
await next()
语句,最后一个中间件函数的await next()
语句是用来执行,外部传递给compose
函数的中间件函数。 - 通过记录上一次执行中间件函数的索引,来与当前执行中间件函数的索引做比较,如果是大于或等于,那就说明在某个中间件函数内多次执行了
await next()
语句,需要抛出错误。