1、准备阶段

下载代码

  1. git clone https://github.com/lxchuan12/koa-compose-analysis.git
  2. cd koa-compose/compose
  3. npm i

2、前置知识了解

先来看一段使用Koa启动服务的代码:

  1. const koa = require('koa');
  2. const app = new koa();
  3. app.use(async (ctx,next) => {
  4. console.log('第一个中间件')
  5. next();
  6. })
  7. app.use(async (ctx,next) => {
  8. console.log('第二个中间件')
  9. next();
  10. })
  11. app.use((ctx,next) => {
  12. console.log('第三个中间件')
  13. next();
  14. })
  15. app.use(ctx => {
  16. console.log('响应');
  17. ctx.body = 'xxxx'
  18. })
  19. app.listen(9999)

可以使用node启动,启动后在浏览器中访问http://localhost:**9999**,会在启动的命令窗口中打印出如下值:

  • 第一个中间件
  • 第二个中间件
  • 第三个中间件
  • 响应

app.use方法就是用来添加中间件的,当执行next()方法会把执行权交给下一个中间件。

其原理大概就是将执行函数放入到一个队列中。

  1. const middleware = []
  2. middleware.push(fn);
  3. middleware.push(fn);
  4. middleware.push(fn);
  5. console.log(middleware) // [fn,fn,fn]

但是这样如何保证中间件函数遇到next()会交出控制权?next()的意义没有体现出来。所以要使用koa-compose模块来控制中间件的执行,就变成了下面这样。

  1. const fn = compose(middleware);

3、源码分析

下载好代码后在第45行打上断点看看里面发生了什么。
image.png

进入package.json文件,运行test命令。
image.png

一步一步debug。
image.png

一步步执行可能跳出到其他文件,这时候不要慌,跳出当前函数即可。
image.png

不到50行代码,但是理解起来不是很容易呀!代码每一行都用注释标了。

  1. 'use strict'
  2. /**
  3. * Expose compositor.
  4. */
  5. module.exports = compose
  6. /**
  7. * Compose `middleware` returning
  8. * a fully valid middleware comprised
  9. * of all those which are passed.
  10. *
  11. * @param {Array} middleware
  12. * @return {Function}
  13. * @api public
  14. */
  15. function compose (middleware) {
  16. // 首先是参数类型检查,不符合就抛错
  17. if (!Array.isArray(middleware)) throw new TypeError('Middleware stack must be an array!')
  18. for (const fn of middleware) {
  19. if (typeof fn !== 'function') throw new TypeError('Middleware must be composed of functions!')
  20. }
  21. /**
  22. * @param {Object} context
  23. * @return {Promise}
  24. * @api public
  25. */
  26. // 返回一个闭包, 保持 middleware 的引用
  27. return function (context, next) {
  28. let index = -1
  29. return dispatch(0) // 从中间件第一项开始执行
  30. function dispatch (i) {
  31. // 一个中间件中有两次next操作会抛出错误
  32. if (i <= index) return Promise.reject(new Error('next() called multiple times'))
  33. index = i // 更新index
  34. let fn = middleware[i]
  35. // 当执行完所有中间件的next,会执行next下面操作
  36. if (i === middleware.length) fn = next //当所有中间件函数都执行完赋值为next,在这里next是undefined
  37. if (!fn) return Promise.resolve() // 这里fn为undefined时,直接返回成功的promise
  38. try {
  39. // 返回Promise,接收fn执行结果
  40. // 注意:dispatch.bind(null, i + 1)不是执行哦,是返回dispatch函数,也就是next
  41. return Promise.resolve(fn(context, dispatch.bind(null, i + 1)))
  42. } catch (err) {
  43. return Promise.reject(err)
  44. }
  45. }
  46. }
  47. }


4、补充

4.1、根据以上的源码分析得到,在一个中间件函数中不能调用两次(),否则会抛出错误。

  1. function one(ctx,next){
  2. console.log('第一个中间件');
  3. next();
  4. next();
  5. }

image.png

4.2、next()调用后返回的是一个Promise对象,可以调用then。

  1. function one(ctx,next){
  2. console.log('第一个中间件');
  3. next().then(function(){
  4. console.log('第一个中间件then')
  5. });
  6. }

4.3、中间件函数内部可以做异步处理,处理得到结果后再进行下一个中间件函数。

  1. function wait (ms) {
  2. return new Promise((resolve) => setTimeout(resolve, ms || 1))
  3. }
  4. function one(ctx,next){
  5. await wait(1)
  6. await next()
  7. }

5、迷惑解答

测试代码的打印结果为什么是[1,2,3,4,5,6]? 它咋就不是[1,2,3,6,5,4]呢?

  1. describe('Koa Compose', function () {
  2. it('should work', async () => {
  3. const arr = []
  4. const stack = []
  5. stack.push(async (context, next) => {
  6. arr.push(1)
  7. await wait(1)
  8. await next()
  9. await wait(1)
  10. arr.push(6)
  11. })
  12. stack.push(async (context, next) => {
  13. arr.push(2)
  14. await wait(1)
  15. await next()
  16. await wait(1)
  17. arr.push(5)
  18. })
  19. stack.push(async (context, next) => {
  20. arr.push(3)
  21. await wait(1)
  22. await next()
  23. await wait(1)
  24. arr.push(4)
  25. })
  26. await compose(stack)({})
  27. expect(arr).toEqual(expect.arrayContaining([1, 2, 3, 4, 5, 6]))
  28. })

解答:当一个中间件调用 next() 则该函数暂停并将控制传递给定义的下一个中间件。当在下游没有更多的中间件执行后,堆栈将展开并且每个中间件恢复执行其上游行为,说白了就是加载完所有中间件后,输出[1,2,3],调用next()后执行完当前中间件,然后把执行权交给上一层中间件。借用一张非常经典的图。

image.png