【若川视野 x 源码共读】第5期 | koa-compose

237 阅读3分钟

本文参加了由公众号@若川视野 发起的每周源码共读活动,点击了解详情一起参与。

1. 学习目标

  • 了解 koa-compose 作用,应对面试官提问koa中间件
  • 学会使用 vsocde 和测试用例 调试源码
  • 学会 jest 部分用法

2. 参考资料

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件事:

    1. 接收一个参数,校验参数是数组,且校验数组中的每一项是函数;
    1. 返回一个函数,这个函数接收两个参数,分别是contextnext,这个函数最后返回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 函数如何使我们能够恰当地利用堆栈流来实现请求和响应流:

image.png

所以,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行的代码,一开始感觉也不复杂;跟着调试代码,咦,比想象中难多了,哈哈哈哈哈~~~

  1. 跟着文章打断点调试之后,发现陷入了无尽的循环中,最后都看得有点懵了;
  2. 通过在不同的地方使用console.log()打印出关键结果之后,结合文章以及别人的笔记,稍微明白点原理了。

另外,这位同学的截图讲解得很详细哦~~ 【第五期】izjing- koa-compose  image.png