koa compose 源码解析
koa
-基于 Node.js
的下一代 web 开发框架。
它最大的特点就是独特的中间件流程控制,是一个典型的洋葱模型。koa 和 koa2 中间件的思路是一样的,但是实现方式有所区别,koa2 在 Node7.6 之后更是可以直接用 async/await
来替代 generator
使用中间件,本文以最后一种情况举例。
本文主要是对 compose 模块的源码解读
源码解读前准备
了解洋葱模型
下面的图是网上找的,很清晰的表明了一个请求是如何经过中间件最后生成响应的,这种模式中开发和使用中间件都是非常方便的。
我们都知道在函数式编程的思想中,compose 是将多个函数合并成一个函数(g() + h() => g(h())),koa 中的 compose 则是将 koa/koa-router
各个中间件合并执行,结合 next()
就形成了下图所示的洋葱模型
koa 示例测试,查看洋葱模型的执行顺序和优点
- 执行顺序测试 我们创建一个 koa 应用
const Koa = require('koa');
const app = new Koa();
app.use(async (ctx, next) => {
console.log('第一个中间件函数')
await next();
console.log('第一个中间件函数next之后!');
})
app.use(async (ctx, next) => {
console.log('第二个中间件函数')
await next();
console.log('第二个中间件函数next之后!');
})
app.use(async ctx => {
ctx.body = 'Hello World';
});
app.listen(3000);
执行命令 node demo1.js
执行结果如下所示:
为什么会是上面这种结果呢,我们带着这些疑问一起去继续往下看,看到最后肯定会能理解。
注意:在使用app.use
将给定的中间件添加到应用程序时,middlewar
(其实就是一个函数)接收两个参数:ctx
和next
。其中next
也是一个函数。
compose 源码解读
compose 代码如下,去掉注释,代码就 25 行,细读确实是很精妙的代码,虽然看着很短,但粗看几层 return
,还是会有点绕。
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) {
//传入的 middleware 参数必须是数组
if (!Array.isArray(middleware)) throw new TypeError('Middleware stack must be an array!')
//middleware 数组的元素必须是函数
for (const fn of middleware) {
if (typeof fn !== 'function') throw new TypeError('Middleware must be composed of functions!')
}
/**
* @param {Object} context
* @return {Promise} 返回一个闭包函数,函数的返回是一个Promise 对象, 保持对 middleware 的引用。
* @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)
}
}
}
}
我们首先去掉条件判断,看下最里面的实际返回
return Promise.resolve(fn(context, dispatch.bind(null, i + 1)));
fn = middleware[i]
也就是某一个中间件,很显然上述代码遍历中间件数组middleware
,依次拿到中间件fn,并执行:
fn(context, dispatch.bind(null, i + 1))
这里可以看到传递给中间件的两个参数:context
和next
函数。
前面我们提到过:在使用app.use
将定的中间件添加到应用程序时,中间件(其实就是一个函数)接收两个参数:ctx
和next
。其中next
也是一个函数。
看到这里是不是清楚了在注册 middwleare
的时候为什么要有两个参数了吧~
回到前面的问题,为什么我们的demo
执行的结果会是上面,我们看第一个中间件,
app.use(async (ctx, next) => {
console.log('第一个中间件函数')
await next();
console.log('第一个中间件函数next之后!');
})
带入到代码中,第一次执行 return dispatch(0)
, 这时第一个中间件被调用,继续展开
dispatch(0)
展开
Promise.resolve((async (ctx, next) => {
console.log('第一个中间件函数')
await next();
console.log('第一个中间件函数next之后');
})(context, dispatch.bind(null, i + 1)));
首先执行 console.log('第一个中间件函数')
没啥毛病, 接下来执行 next()
方法,就跑到第二个中间件去了,所以没有执行第二个 console.log()
app.use(async (ctx, next) => {
console.log('第二个中间件函数')
await next();
console.log('第二个中间件函数next之后!');
})
dispatch(1)
展开
Promise.resolve(async (ctx, next) => Promise.resolve(async (ctx, next) => {
console.log('第一个中间件函数')
Promise.resolve((async (ctx, next) => {
console.log('第二个中间件函数')
await next();
console.log('第二个中间件函数next之后');
})(context, dispatch.bind(null, i + 1)));
console.log('第一个中间件函数next之后')
}))
所以执行 onsole.log('第二个中间件函数')
是不是就很清楚的看出来了。
在第二个中间件执行到await next()
时,同样会轮转到第三个中间件,接下如果有第四个中间件,第五个中间件,聪明的你们会发现,以此类推,直到最后一个中间件。
看到这里,我们会不会很好奇 koa
是怎么调用compose
的呢,等后面的文章再更新~
总结
以上就是我关于 koa compose 的解读和洋葱模型的解析。希望对大家有所帮助,从代码上我们可以看出,洋葱模型也是有所缺陷的,一旦中间件过多,性能还是会有一定的影响的,所以我们需要结合自己的项目场景作出合适的选择。
如果以上有问题,欢迎大家留言,一起探讨,谢谢!。