本文参加了由公众号@若川视野 发起的每周源码共读活动,点击了解详情一起参与。
1. 学习目标
- 了解 koa-compose 作用,应对面试官提问koa中间件
- 学会使用 vsocde 和测试用例 调试源码
- 学会 jest 部分用法
2. 参考资料
-
koa-compose 涉及到的设计模式叫职责链模式~
-
可以翻阅《JavaScript设计模式与开发实践》第十三章
-
koa源码,可以参考:学习 koa 源码的整体架构,浅析koa洋葱模型原理和co原理
3. 学习过程
3.1 克隆代码
git clone https://github.com/lxchuan12/koa-compose-analysis.git
cd koa-compose/compose
npm i
3.2 代码调试过程
参考这位同学的笔记,图文很详细哦。 【第五期】izjing- koa-compose
3.3 koa-compose源码
'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) {
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 {
return Promise.resolve(fn(context, dispatch.bind(null, i + 1)))
} catch (err) {
return Promise.reject(err)
}
}
}
}
compose函数主要做了2件事:
-
- 接收一个参数,校验参数是数组,且校验数组中的每一项是函数;
-
- 返回一个函数,这个函数接收两个参数,分别是
context和next,这个函数最后返回Promise。
- 返回一个函数,这个函数接收两个参数,分别是
3.3.1 dispatch 函数
function dispatch (i) {
// 一个函数中多次调用报错
// await next()
// await next()
if (i <= index) return Promise.reject(new Error('next() called multiple times'))
index = i
// 取出数组里的 fn1, fn2, fn3...
let fn = middleware[i]
// 最后 相等,next 为 undefined
if (i === middleware.length) fn = next
// 直接返回 Promise.resolve()
if (!fn) return Promise.resolve()
try {
return Promise.resolve(fn(context, dispatch.bind(null, i + 1)))
} catch (err) {
return Promise.reject(err)
}
}
复制代码
值得一提的是:bind函数是返回一个新的函数。第一个参数是函数里的this指向(如果函数不需要使用this,一般会写成null)。 这句fn(context, dispatch.bind(null, i + 1),i + 1 是为了 let fn = middleware[i] 取middleware中的下一个函数。 也就是 next 是下一个中间件里的函数。也就能解释上文中的 gif图函数执行顺序。 测试用例中数组的最终顺序是[1,2,3,4,5,6]。
3.3.2 简化 compose 便于理解
compose 执行后就是类似这样的结构(省略 try catch 判断)。koa-compose是将app.use添加到middleware数组中的中间件(函数),通过使用Promise串联起来,next()返回的是一个promise。
// 这样就可能更好理解了。
// simpleKoaCompose
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();
})
)
})
)
})
);
};
fnMiddleware(ctx).then(handleResponse).catch(onerror);
也就是说
koa-compose返回的是一个Promise,从中间件(传入的数组)中取出第一个函数,传入context和第一个next函数来执行。
第一个next函数里也是返回的是一个Promise,从中间件(传入的数组)中取出第二个函数,传入context和第二个next函数来执行。
第二个next函数里也是返回的是一个Promise,从中间件(传入的数组)中取出第三个函数,传入context和第三个next函数来执行。
第三个...
以此类推。最后一个中间件中有调用next函数,则返回Promise.resolve。如果没有,则不执行next函数。 这样就把所有中间件串联起来了。这也就是我们常说的洋葱模型。\
3.3.3 那么koa洋葱模型怎么实现的呢?
洋葱模型图如下图所示:
在koa中,请求响应都放在中间件的第一个参数context对象中了。
再引用Koa中文文档中的一段:
如果您是前端开发人员,您可以将 next(); 之前的任意代码视为“捕获”阶段,这个简易的 gif 说明了 async 函数如何使我们能够恰当地利用堆栈流来实现请求和响应流:
所以,koa洋葱模型的实现是:app.use() 把中间件函数存储在middleware数组中,最终会调用koa-compose导出的函数compose返回一个promise,中间函数的第一个参数context是包含响应和请求的一个对象,会不断传递给下一个中间件。next是一个函数,返回的是一个promise。
3.3.4 next函数为什么不能调用多次?
原因是 compose 函数中的 dispatch 函数中加了限制,代码如下:
let index = -1
return dispatch(0)
function dispatch(i) {
if (i <= index) return Promise.reject(new Error('next() called multiple times'))
index = i
......
调用两次后 i 和 index 都为 1,所以会报错。
4. 总结
自己看着50行的代码,一开始感觉也不复杂;跟着调试代码,咦,比想象中难多了,哈哈哈哈哈~~~
- 跟着文章打断点调试之后,发现陷入了无尽的循环中,最后都看得有点懵了;
- 通过在不同的地方使用
console.log()打印出关键结果之后,结合文章以及别人的笔记,稍微明白点原理了。
另外,这位同学的截图讲解得很详细哦~~
【第五期】izjing- koa-compose