1、准备阶段
下载代码
git clone https://github.com/lxchuan12/koa-compose-analysis.git
cd koa-compose/compose
npm i
2、前置知识了解
先来看一段使用Koa启动服务的代码:
const koa = require('koa');
const app = new koa();
app.use(async (ctx,next) => {
console.log('第一个中间件')
next();
})
app.use(async (ctx,next) => {
console.log('第二个中间件')
next();
})
app.use((ctx,next) => {
console.log('第三个中间件')
next();
})
app.use(ctx => {
console.log('响应');
ctx.body = 'xxxx'
})
app.listen(9999)
可以使用node启动,启动后在浏览器中访问http://localhost:**9999**,会在启动的命令窗口中打印出如下值:
- 第一个中间件
- 第二个中间件
- 第三个中间件
- 响应
app.use方法就是用来添加中间件的,当执行next()方法会把执行权交给下一个中间件。
其原理大概就是将执行函数放入到一个队列中。
const middleware = []
middleware.push(fn);
middleware.push(fn);
middleware.push(fn);
console.log(middleware) // [fn,fn,fn]
但是这样如何保证中间件函数遇到next()会交出控制权?next()的意义没有体现出来。所以要使用koa-compose模块来控制中间件的执行,就变成了下面这样。
const fn = compose(middleware);
3、源码分析
下载好代码后在第45行打上断点看看里面发生了什么。
进入package.json文件,运行test命令。
一步一步debug。
一步步执行可能跳出到其他文件,这时候不要慌,跳出当前函数即可。
不到50行代码,但是理解起来不是很容易呀!代码每一行都用注释标了。
'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
*/
// 返回一个闭包, 保持 middleware 的引用
return function (context, next) {
let index = -1
return dispatch(0) // 从中间件第一项开始执行
function dispatch (i) {
// 一个中间件中有两次next操作会抛出错误
if (i <= index) return Promise.reject(new Error('next() called multiple times'))
index = i // 更新index
let fn = middleware[i]
// 当执行完所有中间件的next,会执行next下面操作
if (i === middleware.length) fn = next //当所有中间件函数都执行完赋值为next,在这里next是undefined
if (!fn) return Promise.resolve() // 这里fn为undefined时,直接返回成功的promise
try {
// 返回Promise,接收fn执行结果
// 注意:dispatch.bind(null, i + 1)不是执行哦,是返回dispatch函数,也就是next
return Promise.resolve(fn(context, dispatch.bind(null, i + 1)))
} catch (err) {
return Promise.reject(err)
}
}
}
}
4、补充
4.1、根据以上的源码分析得到,在一个中间件函数中不能调用两次(),否则会抛出错误。
function one(ctx,next){
console.log('第一个中间件');
next();
next();
}
4.2、next()调用后返回的是一个Promise对象,可以调用then。
function one(ctx,next){
console.log('第一个中间件');
next().then(function(){
console.log('第一个中间件then')
});
}
4.3、中间件函数内部可以做异步处理,处理得到结果后再进行下一个中间件函数。
function wait (ms) {
return new Promise((resolve) => setTimeout(resolve, ms || 1))
}
function one(ctx,next){
await wait(1)
await next()
}
5、迷惑解答
测试代码的打印结果为什么是[1,2,3,4,5,6]? 它咋就不是[1,2,3,6,5,4]呢?
describe('Koa Compose', function () {
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]))
})
解答:当一个中间件调用 next() 则该函数暂停并将控制传递给定义的下一个中间件。当在下游没有更多的中间件执行后,堆栈将展开并且每个中间件恢复执行其上游行为,说白了就是加载完所有中间件后,输出[1,2,3],调用next()后执行完当前中间件,然后把执行权交给上一层中间件。借用一张非常经典的图。