【源码共读】 | koa-compose

712 阅读2分钟

本文已参与「新人创作礼」活动,一起开启掘金创作之路。

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

【若川视野 x 源码共读】第5期 | koa-compose 点击了解本期详情一起参与

本文涉及

koa洋葱模型原理

bind函数

Promise函数

koa中间件的执行顺序

源码地址:github.com/koajs/compo…

若川大佬的仓库:github.com/lxchuan12/k…

我们先看一下package.json

npm run test

image-20220920171201041

可以看到,这个库有很多的测试用例,我们选择第一个来调试

image-20220920171320858

以第一个测试用例为例

 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是什么,改变了arr的插入顺序
  • context如何传递

源码分析

function compose(middleware) {
  // 检查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 {
        // 指向下一个next
        return Promise.resolve(fn(context, dispatch.bind(null, i + 1)))
      } catch (err) {
        return Promise.reject(err)
      }
    }
  }
}

通过源码分析,程序大致分为几步执行

  • 检查middleware是否是数组,检查数组中每一个元素是否是函数

  • 返回一个dispatch函数

  • dispatch函数中,按顺序取出中间件执行,然后dispatch指向下一个中间件


  • dispatch就是next(),即执行下一个中间件

  • contextPromise.resolve(fn(context, dispatch.bind(null, i + 1)))中一直传递下去

综上,我们可以对刚才的测试用例进行分析

it('should work', async () => {
  const arr = []
  const stack = []

  stack.push(async (context, next) => {
    arr.push(1)
    await wait(1)
    // 等待 执行下一层的函数  arr.push(2)
    // arr = [1]
    await next()
    await wait(1)
    // arr = [1,2,3,4,5,6]
    arr.push(6)
  })

  stack.push(async (context, next) => {
    arr.push(2)
    await wait(1)
    // 等待 执行下一层的函数  arr.push(3)
    // arr = [1,2]
    await next()
    await wait(1)
    // arr = [1,2,3,4,5]
    arr.push(5)
  })

  stack.push(async (context, next) => {
    arr.push(3)
    await wait(1)
    // 等待 执行下一层的函数,此时next===undefined 跳出,继续执行下面的代码
    // arr = [1,2,3]
    await next()
    await wait(1)
    // arr = [1,2,3,4]
    arr.push(4)
  })

  await compose(stack)({})
  expect(arr).toEqual(expect.arrayContaining([1, 2, 3, 4, 5, 6]))
})

这就是koa的洋葱模型

image-20220921160223060

总结

通过这个小的库的源码分析,我们知道了koa中的洋葱模型的实现原理,在返回函数中将当前的context和下一个中间件函数作为参数传递,并执行了当前的fn,通过bind将指针指向下一个中间件,继续迭代

这个库代码简短,但是涉及到了很多内容,闭包,bindPromise的运用,通过这几个的组合将中间件串联起来执行,非常的巧妙

参考文章

juejin.cn/post/700537…

juejin.cn/post/701203…

github.com/niexq/koaCo…