Compose middleware specifically for koa.
提供了用于组织中间件函数的方法
为每个中间件加入了共用的 ctx 和 用于转移到下个中间件的 next 方法
JavaScriptconst 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" })
CODE{ 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
下面是一个基本的测试 case
JavaScriptit('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])) })
JavaScriptfunction 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 的,因为函数调用本身就在转移控制
JavaScriptit.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])) })
JavaScriptfunction 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 的参数和返回值都不关注,它的作用只是用来控制程序流
从结果上来看,只考虑主分支,会形成如下的效果
JavaScriptconst [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 的场景,不是必要的的,因为
参考
JavaScriptconst a = await 1 // -> let a Promise.resolve(1).then(v => a = v)
只有这个 case 没通过
JavaScriptit('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
.then
function - that's the only standard promise libraries use.
看注释吧
JavaScript/** - 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 后执行 这点 可以结合下面理解下
JavaScriptit("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