blog
blog copied to clipboard
koa-compose 解读笔记
koa-compose 作为koa实现中间件串联功能的关键函数,值得我们细细品味,话不多说先送上👉源码,别惊讶确实只有这么多行....
先撸一遍
'use strict'
/**
* Expose compositor.
*/
module.exports = compose
/**
* Compose `middleware` returning
* a fully valid middleware comprised
* of all those which are passed.
* 👉 原文译: 将所有中间件组合,返回一个包含所有传入中间件的函数
*
* @param {Array} middleware
* @return {Function}
* @api public
*/
function compose (middleware) {
// 传入middware的必须为数组
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 fnMiddleware(ctx).then(handleResponse).catch(onerror); 时并没有传入第二个next参数,当然也传入不了
return function (context, next) {
// last called middleware #
// 这里的 index 是用于防止在一个中间件中重复调用 next() 函数,初始值设置为 -1
let index = -1
// 启动递归,遍历所有中间件
return dispatch(0)
// 递归包装每一个中间件,并且统一输出一个 Promise 对象
function dispatch (i) {
// 注意随着 next() 执行,i、index + 1、当前中间件的执下标,在进入每个中间件的时候会相等
// 每执行一次 next (或者dispatch) 上面三个值都会加 1
/* 原理说明:
* 当一个中间件中调用了两次 next方法,第一次next调用完后,洋葱模型走完,index的值从 -1 变到了 middlewares.length,
* 此时第一个某个中间件中的 next 再被调用,那么当时传入的 i + 1 的值,必定是 <= middlewares.length 的
*/
if (i <= index) return Promise.reject(new Error('next() called multiple times'))
// 通过校验后,index 与 i 的值同步
index = i
// 取出一个中间件函数
let fn = middleware[i]
// 若执行到了最后一个,(其实此时的next也一定为undefined),我认为作者是为何配合下一句一起判断中间件的终结
if (i === middleware.length) fn = next
// 遍历到了最后一个中间件,则返回一个 resolve 状态的 Promise 对象
if (!fn) return Promise.resolve()
try {
// 递归执行每一个中间件,当前中间件的 第二个 入参为下一个中间件的 函数句柄
// 这里注意:每一个 next 函数,都是下一个 dispatch函数,而这个函数会返回一个 Promise 对象
return Promise.resolve(fn(context, dispatch.bind(null, i + 1)));
} catch (err) {
// 中间件执行过程中出错的异常捕获
return Promise.reject(err)
}
}
}
}
接下来是手摸手教你写 compose , 要是上面的看懂了,就节省时间不必往下看了 👻👻👻
拆解分析
条件判断
前面的一些类型判断语句也就不做过多描述。
if (!Array.isArray(middlewares)) throw new TypeError('Middlewares must be an array')
for (const fn of middlewares) {
if (typeof fn !== 'function') {
throw new TypeError('item of middlewares must be an functions')
}
}
第一阶段 - 递归与洋葱模型
- 是一个作用是将所有的中间件串联起来,包装成一个函数,并且返回。这里要使用高阶函数
- 递归就是天然的洋葱模型实现
return function (context) {
function dispatch(i) {
let fn = middlewares[i] // ① 取出当前的中间件,fn指向每一个中间件
if (!fn) return // ④ 为递归设定终结条件
fn(context) // ② 执行当前中间件
return dispatch(i + 1)// ③ 形成初步的递归调用
}
return dispatch(0) // ④ 设定一个递归启动点
}
// 简单准备第三个中间件
const mid1 = () => console.log('mid1')
const mid2 = () => console.log('mid2')
const mid3 = () => console.log('mid3')
const fnx = compose([mid1, mid2, mid3])
fnx() // mid3 mid2 mid1
第二阶段 - 支持异步
- 使用
next
表示开启下个中间件的函数句柄 - 使用
bind
对dispatch
进行函数改造
return function (context, next) {
function dispatch(i) {
let fn = middlewares[i]
if (!fn) return
// ⑤ 改造当前中间件执行时传入的参数,将下一个中间件的含数句柄,作为第二个参数 next 传入
return fn(context, dispatch.bind(null, i + 1))
}
return dispatch(0)
}
const mid1 = (ctx, next) => {
console.log('mid1')
setTimeout(()=>{
console.log('mid1 wait for 2s')
next()
}, 2000)
console.log('mid1 after')
}
const mid2 = (ctx, next) => {
console.log('mid2')
setTimeout(()=>{
console.log('mid2 wait for 2s')
next()
},2000)
console.log('mid2 after')
}
const mid3 = function (ctx, next) {
console.log('mid3')
console.log('mid3 after')
}
const fnx = compose([mid1, mid2, mid3])
fnx() // 输出结果我就不写了,你猜猜是什么
第三阶段 - 支持 thenable
研究清楚第二阶段的测试输出后,我们基本将异步中间件
串联起来。那么源码中,dispatch
函数,无论走哪一个分支,为何一定都要返回一个Promise
对象呢?
想了好久不得其解,就把源码中返回Promise
部分改为同步,跑了一下koa
自带的测试用例。
# koa
$ npm run test
1) app.context
should merge properties:
Uncaught TypeError: Cannot read property 'then' of undefined
at Application.handleRequest (lib/application.js:166:29) // 👈👈 点开这里看了看
at Server.handleRequest (lib/application.js:148:19)
at parserOnIncoming (_http_server.js:779:12)
at HTTPParser.parserOnHeadersComplete (_http_common.js:117:17)
[use `--full-trace` to display the full stack trace]
// application.js #line 160
handleRequest(ctx, fnMiddleware) {
const res = ctx.res;
res.statusCode = 404;
const onerror = err => ctx.onerror(err);
const handleResponse = () => respond(ctx);
onFinished(res, onerror);
// 上面的错误堆栈,追踪到的就是这里 #line 166 👇👇👇
return fnMiddleware(ctx).then(handleResponse).catch(onerror);
}
// application.js #line 141
callback() {
const fn = compose(this.middleware);
if (!this.listenerCount('error')) this.on('error', this.onerror);
const handleRequest = (req, res) => {
const ctx = this.createContext(req, res);
return this.handleRequest(ctx, fn);
};
return handleRequest;
}
不难看出抛出错误的#line 161
fnMiddleware指的就是 compose
之后的结果。说明在koa
中,将所有中间件串联起来之后,希望得到的是一个thenable
的对象。我们则需要继续添加对Promise
的支持
return function (context, next) {
function dispatch(i) {
let fn = middlewares[i]
if (i === middlewares.length) fn = next
// ⑧ 调用最后一个中间件
if (!fn) return Promise.resolve()
try {
// ⑥ 成功调用
return Promise.resolve(fn(context, dispatch.bind(null, i + 1)))
} catch (err) {
// ⑦ 成功过程出错
return Promise.reject()
}
}
return dispatch(0)
}
防止多次调用 next 的调用次数
我们知道一次深入到最内层,再原路返回到最外层,就是一次完整的洋葱模型。对于代码设计中的index
与 i
的关系,也是一个设计巧妙的宝盒。如下测试代码,在mw1中调用多次next函数
async function mw1 (context, next) {
console.log('===== middleware 1 =====')
next()
next() // 预计这里是会爆出一个错误,但是为什么呢?是如何工作的呢?
}
function mw2 (context, next) {
console.log('===== middleware 2 =====')
next()
}
async function mw3 (context, next) {
console.log('===== middleware 3 =====')
}
如上图我们可以知道,用index
去标记i
曾经到达过的最深层词的中间件的下标,那么就能有效防止再原路返回时,每个中间件再次出触发next
深入深层次的情况。
return function (context, next) {
let index = -1 // ⑨ 表示初始的层次
function dispatch(i) {
// 10 当前调用的层次,是否小于曾经到过的最大层次(变相判断这一个中间件的next是否已经调用过了)
if (i <= index) return Promise.reject('next cant not be invoke multiple time')
// 11 通过了上面的校验,就标记本次到达的最深层次
index = i
let fn = middlewares[i]
if (i === middlewares.length) fn = next
if (!fn) return Promise.resolve()
try {
return Promise.resolve(fn(context, dispatch.bind(null, i + 1)))
} catch (err) {
return Promise.resolve()
}
}
return dispatch(0)
}
compose 结果
若还是不太明白上面写法的原理,那我们来看看compose
组合 middlewares
后的结果会是什么样子。
const [mid1, mid2, mid3] = middlewares
// compose 可以理解为
const fnMiddleware = function(ctx) {
return Promise.resolve(
mid1(ctx, function next () {
return Promise.resolve(
mid2(ctx, function next () {
return Promise.resolve(
mid3(ctx, function next() {
return Promise.resolve()
})
)
})
)
})
)
}
参考资料
[1] Koa源码 - github
[2] 大家觉得 Koa 框架还有什么不足的地方吗? - Starkwang的回答 - 知乎