本文已参与「新人创作礼」活动,一起开启掘金创作之路。
本文参加了由公众号@若川视野 发起的每周源码共读活动,点击了解详情一起参与。
【若川视野 x 源码共读】第5期 | koa-compose 点击了解本期详情一起参与。
本文涉及
koa洋葱模型原理
bind函数
Promise函数
koa中间件的执行顺序
若川大佬的仓库:github.com/lxchuan12/k…
我们先看一下package.json
npm run test
可以看到,这个库有很多的测试用例,我们选择第一个来调试
以第一个测试用例为例
it('should work', async () => {
const arr = []
const stack = []
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)({})
expect(arr).toEqual(expect.arrayContaining([1, 2, 3, 4, 5, 6]))
})
疑问点
- 中间件是如何加载的
- 中间件的执行顺序
- next是什么,改变了
arr的插入顺序 context如何传递
源码分析
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!')
}
/**
* @param {Object} context
* @return {Promise}
* @api public
*/
return function (context, next) {
// last called middleware #
let index = -1
return dispatch(0)
function dispatch(i) {
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
return Promise.resolve(fn(context, dispatch.bind(null, i + 1)))
} catch (err) {
return Promise.reject(err)
}
}
}
}
通过源码分析,程序大致分为几步执行
-
检查
middleware是否是数组,检查数组中每一个元素是否是函数 -
返回一个
dispatch函数 -
在
dispatch函数中,按顺序取出中间件执行,然后dispatch指向下一个中间件
-
dispatch就是next(),即执行下一个中间件 -
context在Promise.resolve(fn(context, dispatch.bind(null, i + 1)))中一直传递下去
综上,我们可以对刚才的测试用例进行分析
it('should work', async () => {
const arr = []
const stack = []
stack.push(async (context, next) => {
arr.push(1)
await wait(1)
// 等待 执行下一层的函数 arr.push(2)
// arr = [1]
await next()
await wait(1)
// arr = [1,2,3,4,5,6]
arr.push(6)
})
stack.push(async (context, next) => {
arr.push(2)
await wait(1)
// 等待 执行下一层的函数 arr.push(3)
// arr = [1,2]
await next()
await wait(1)
// arr = [1,2,3,4,5]
arr.push(5)
})
stack.push(async (context, next) => {
arr.push(3)
await wait(1)
// 等待 执行下一层的函数,此时next===undefined 跳出,继续执行下面的代码
// arr = [1,2,3]
await next()
await wait(1)
// arr = [1,2,3,4]
arr.push(4)
})
await compose(stack)({})
expect(arr).toEqual(expect.arrayContaining([1, 2, 3, 4, 5, 6]))
})
这就是koa的洋葱模型
总结
通过这个小的库的源码分析,我们知道了koa中的洋葱模型的实现原理,在返回函数中将当前的context和下一个中间件函数作为参数传递,并执行了当前的fn,通过bind将指针指向下一个中间件,继续迭代
这个库代码简短,但是涉及到了很多内容,闭包,bind,Promise的运用,通过这几个的组合将中间件串联起来执行,非常的巧妙