1、准备阶段
下载代码
git clone https://github.com/lxchuan12/koa-compose-analysis.gitcd koa-compose/composenpm 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 = -1return dispatch(0) // 从中间件第一项开始执行function dispatch (i) {// 一个中间件中有两次next操作会抛出错误if (i <= index) return Promise.reject(new Error('next() called multiple times'))index = i // 更新indexlet fn = middleware[i]// 当执行完所有中间件的next,会执行next下面操作if (i === middleware.length) fn = next //当所有中间件函数都执行完赋值为next,在这里next是undefinedif (!fn) return Promise.resolve() // 这里fn为undefined时,直接返回成功的promisetry {// 返回Promise,接收fn执行结果// 注意:dispatch.bind(null, i + 1)不是执行哦,是返回dispatch函数,也就是nextreturn 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()后执行完当前中间件,然后把执行权交给上一层中间件。借用一张非常经典的图。

