koa-compose 代码分析
背景、为什么要了解这个
- 学习优雅的代码实现
- 这个库的实现代码量很少,并伴有完善的测试
- 为 koa 的理解做铺垫
是什么
Compose middleware specifically for koa.
提供了用于组织中间件函数的方法
为每个中间件加入了共用的 ctx 和 用于转移到下个中间件的 next 方法
const compose = require("./index")
const consoleWithTabsize = (tabsize, ...args) => {
console.log("\t".repeat(tabsize), ...args)
}
const fn1 = (ctx, next) => {
console.log({ ctx })
consoleWithTabsize(0, ">>> fn1")
next()
consoleWithTabsize(0, "<<< fn1")
}
const fn2 = (ctx, next) => {
consoleWithTabsize(1, ">>> fn2")
next()
consoleWithTabsize(1, "<<< fn2")
}
const fn3 = (ctx, next) => {
consoleWithTabsize(2, ">>> fn3")
next()
consoleWithTabsize(2, "<<< fn3")
}
compose([fn1, fn2, fn3])({
name: "this is ctx"
})
{ ctx: { name: 'this is ctx' } }
>>> fn1
>>> fn2
>>> fn3
<<< fn3
<<< fn2
<<< fn1
从上面可以看出,调用 next 实际上就是去调用下一个 中间件函数(也就是有些文章中所说的转移控制权)
下图摘自 koa 中文 官网 https://github.com/demopark/koa-docs-Zh-CN/blob/master/guide.md#%E7%BC%96%E5%86%99%E4%B8%AD%E9%97%B4%E4%BB%B6

代码
仓库在这里 https://github.com/koajs/compose 推荐本地拉下来仓库,进行手动断点理解 https://github.com/koajs/compose/blob/master/test/test.js
base case
下面是一个基本的测试 case
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]))
})
function compose(middleware) {
return function (context, next) {
return dispatch(0)
function dispatch(i) {
const fn = middleware[i]
if (!fn) return Promise.resolve()
return Promise.resolve(fn(context, dispatch.bind(null, i + 1)))
}
}
}
这段代码是 koa-compose 的核心逻辑,我删除了原有代码中的分支代码
compose 的输入是一个函数组成的数组 middleware,输出是一个接受 context【一个对象】 和 next 【一个函数】的函数
context 对象会在 middleware 中的所有函数中的第一个参数拿到
next 则是对下一个中间件函数的调用函数,对应代码的 dispatch.bind(null, i + 1)
重点说一下为啥要 return 其实如果我们传入的中间件中不会用到 Promise 或着 async await 的写法 不 return 也是 OK 的,因为函数调用本身就在转移控制
it.only("should work in not return condition", async () => {
const arr = []
const stack = []
stack.push((context, next) => {
arr.push(1)
next()
arr.push(6)
})
stack.push((context, next) => {
arr.push(2)
next()
arr.push(5)
})
stack.push((context, next) => {
arr.push(3)
next()
arr.push(4)
})
await compose(stack)({})
expect(arr).toEqual(expect.arrayContaining([1, 2, 3, 4, 5, 6]))
})
function compose(middleware) {
return function (context, next) {
return dispatch(0)
function dispatch(i) {
const fn = middleware[i]
if (!fn) return
fn(context, dispatch.bind(null, i + 1))
}
}
}
但我们 should work case 就跑不通了
究其原因是,我们需要等待 next() 这个函数调用完,如果其中存在 Promise 的执行流,也要能让外界知道他的执行情况
所以必须要 return,我们调用 next 的时候,通常也会进行 await next() ,next 的参数和返回值都不关注,它的作用只是用来控制程序流
从结果上来看,只考虑主分支,会形成如下的效果
const [fn1, fn2, fn3] = stack;
const fnMiddleware = function(context){
return Promise.resolve(
fn1(context, function next(){
return Promise.resolve(
fn2(context, function next(){
return Promise.resolve(
fn3(context, function next(){
return Promise.resolve();
})
)
})
)
})
);
};
至于 return Promise.resolve(fn(context, dispatch.bind(null, i + 1))) 中 Promise.resolve 包一层是否必要
如果我们只考虑 await next 的场景,不是必要的的,因为
参考
const a = await 1
// ->
let a
Promise.resolve(1).then(v => a = v)
只有这个 case 没通过
it('should create next functions that return a Promise', function () {
const stack = []
const arr = []
for (let i = 0; i < 5; i++) {
stack.push((context, next) => {
arr.push(next())
})
}
compose(stack)({})
for (const next of arr) {
assert(isPromise(next), 'one of the functions next is not a Promise')
}
})
function isPromise (x) {
return x && typeof x.then === 'function'
}
https://stackoverflow.com/questions/27746304/how-to-check-if-an-object-is-a-promise
If it has a
.thenfunction - that’s the only standard promise libraries use.
边界情况
看注释吧
/**
- Compose `middleware` returning
- a fully valid middleware comprised
- of all those which are passed.
*
- @param {Array} middleware
- @return {Function}
- @api public
*/
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!")
}
/**
- @param {Object} context
- @return {Promise}
- @api public
*/
return function (context, next) {
// last called middleware #
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]
// 支持 compose([fn1, fn2, fn3])({}, fn4) 的写法,fn4 会在 fn3 后执行
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)
}
}
}
}
对于 支持 compose([fn1, fn2, fn3])({}, fn4) 的写法,fn4 会在 fn3 后执行 这点 可以结合下面理解下
it("should work 2", 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)({}, async (_, next) => {
arr.push(99.1)
await next()
arr.push(99.2)
})
expect(arr).toEqual(expect.arrayContaining([1, 2, 3, 99.1, 99.2, 4, 5, 6]))
})
参考
https://github.com/lxchuan12/koa-compose-analysis https://bytedance.feishu.cn/wiki/wikcnnAWRea37N3fBa8cQOlkP4N