前言
- 本文参加了由公众号@若川视野 发起的每周源码共读活动, 点击了解详情一起参与。
- 这是源码共读的第5期,链接:koa-compose
什么是中间件?
中间件本质是一个函数,程序输入值到输出值之间运用中间件可以进行各种加工。
Koa 采用独特的"洋葱模型"中间件机制: 注意这个“洋葱模型”和递归执行斐波拉契数列有点类似
请求
↓
中间件1 (开始)
↓
中间件2 (开始)
↓
中间件3 (开始)
↓
中间件3 (结束)
↓
中间件2 (结束)
↓
中间件1 (结束)
↓
响应
// 递归执行斐波拉契数列
const fb = (n) => {
if(n === 1 || n === 2) return 1
return fb(n - 1) + fb(n - 2)
}
fb(10) // 5
koa-compose基础使用
思考 ?为啥arr的输出值是[1,2,3,4,5,6]
const arr = []
const stack = []
// 中间件函数1
stack.push(async (context, next) => {
// arr的值为[1]
arr.push(1)
// 执行next函数,此时next函数里面会干什么呢 ?
await next()
arr.push(6)
})
// 中间件函数2
stack.push(async (context, next) => {
// 此时arr的值为[1, 2]
arr.push(2)
await next()
arr.push(5)
})
// 中间件函数3
stack.push(async (context, next) => {
// 此时arr的值为[1, 2, 3]
arr.push(3)
// next里面已经没有中间件函数需要执行了拉,等待next执行完成后,arr.push(4)执行
await next()
// 此时arr的值为[1, 2, 3, 4]
arr.push(4)
})
await compose(stack)({})
console.log(arr) // [1,2,3,4,5,6]
带着上面的疑问我们来看下面的源码解析
koa-compose源码解析
1. composeSlim函数
composeSlim是一个compose的简略函数,功能相同,用于production环境
- a. composeSlim返回了一个async(ctx, next) => {...}函数,假设为函数A
- b. 执行函数A,用户可以传参ctx和next给A函数
- c. A函数内部执行dispatch(0)(),注意有2个括号,第一个括号是执行dispacth(0),将i = 0传入进去,i 被缓存到了函数作用域中,此时dispacth(0)返回一个函数async () => {...},假设为函数B,第二个()就是执行该B函数
- d. 此时js执行栈进入B函数中执行,B函数重点是获取fn函数并执行它。如果middleware[i]存在值,fn等于middleware[i], 否则为next函数, 当fn为空时,请阅读 f 点解析。
- e fn函数被执行,fn函数就是用户写的中间件函数,这里的重点是fn会将dispatch(i+1)作为传参,也就是我们上文中“koa-compose基础使用”示例中的next函数,用户在中间件函数fn内部手动调用next函数,next函数执行时又拿到下一个中间件函数,下一个被执行,继续传参next函数,形成了套娃...直到fn为空,看接下来的 f 点解析
- f. 当fn为空时,函数return,最后一个中间件函数体中的await next()就执行完成,参照“koa-compose基础使用”示例中间件函数3,然后执行arr.push(4), 至此中间件函数3全部执行完成,然后中间件函数2的await next()也就执行完成了....以此类推,所有中间件函数执行完成
/**
* @param {Array} middleware 中间件函数数组
* @return {Function}
*/
const composeSlim = (middleware) => async (ctx, next) => {
const dispatch = (i) => async () => {
const fn = i === middleware.length
? next
: middleware[i]
if (!fn) return
return await fn(ctx, dispatch(i + 1))
}
return dispatch(0)()
}
/** @typedef {import("koa").Middleware} Middleware */
/**
* Compose `middleware` returning
* a fully valid middleware comprised
* of all those which are passed.
*
* @param {...(Middleware | Middleware[])} middleware 中间件数组
* @return {Middleware}
*/
const compose = (...middleware) => {
const funcs = middleware.flat()
// 遍历传参,保证传参都是数组
for (const fn of funcs) {
if (typeof fn !== 'function') throw new TypeError('Middleware must be composed of functions!')
}
if (process.env.NODE_ENV === 'production') return composeSlim(funcs)
....
}
2. compose函数
和上文composeSlim函数一样的逻辑思路
const compose = (...middleware) => {
// 将传参拍平
const funcs = middleware.flat()
for (const fn of funcs) {
if (typeof fn !== 'function') throw new TypeError('Middleware must be composed of functions!')
}
if (process.env.NODE_ENV === 'production') return composeSlim(funcs)
// 返回一个函数
return async (ctx, next) => {
const dispatch = async (i) => {
// 取出下一个执行的函数fn
const fn = i === funcs.length
? next
: funcs[i]
if (!fn) return
let nextCalled = false
let nextResolved = false
/**
* 异步函数,用于执行下一步操作
*
* @returns 返回下一步操作的结果
* @throws 当next()被多次调用时,抛出错误
*/
const nextProxy = async () => {
// nextCalled是确保next()函数只被调用一次
if (nextCalled) throw Error('next() called multiple times')
nextCalled = true
try {
// 执行dispatch(i + 1)
return await dispatch(i + 1)
} finally {
nextResolved = true
}
}
const result = await fn(ctx, nextProxy)
if (nextCalled && !nextResolved) {
throw Error(
'Middleware resolved before downstream.\n\tYou are probably missing an await or return'
)
}
return result
}
// 执行dispatch(0)
return dispatch(0)
}
}
用promise如何实现示例效果
koa-compose基础使用示例的结构就类似于如下代码
M1的resolved状态依赖M2的resolved状态,M2依赖M3...
等最里面的resolved之后,依次往外传递。和洋葱好像~
const arr = []
const fun1 = () => new Promise((resolve) => resolve(1))
const fun2 = () => new Promise((resolve) => resolve(2))
const fun3 = () => new Promise((resolve) => resolve(3))
fun1().then(() => { // M1
arr.push(1)
return fun2().then(() => { // M2
arr.push(2)
return fun3().then(() => { // M3
arr.push(3)
}).then(() => { // M4
arr.push(4)
})
}).then(() => { // M5
arr.push(5)
})
}).then(() => { //M6
arr.push(6)
console.log(arr) // [1,2,3,4,5,6]
})
总结
之前对中间件的认识都是模糊的状态,通过阅读源码终于理清楚啦,好开心~
如有问题,欢迎指正~