Last updated on

koa-compose 代码分析

背景、为什么要了解这个

  1. 学习优雅的代码实现
  2. 这个库的实现代码量很少,并伴有完善的测试
  3. 为 koa 的理解做铺垫

是什么

Compose middleware specifically for koa.

提供了用于组织中间件函数的方法

为每个中间件加入了共用的 ctx 和 用于转移到下个中间件的 next 方法

const compose = require("./index")

const consoleWithTabsize = (tabsize, ...args) => {
  console.log("\t".repeat(tabsize), ...args)
}

const fn1 = (ctx, next) => {
  console.log({ ctx })
  consoleWithTabsize(0, ">>> fn1")
  next()
  consoleWithTabsize(0, "<<< fn1")
}
const fn2 = (ctx, next) => {
  consoleWithTabsize(1, ">>> fn2")
  next()
  consoleWithTabsize(1, "<<< fn2")
}

const fn3 = (ctx, next) => {
  consoleWithTabsize(2, ">>> fn3")
  next()
  consoleWithTabsize(2, "<<< fn3")
}

compose([fn1, fn2, fn3])({
  name: "this is ctx"
})
{ ctx: { name: 'this is ctx' } }
 >>> fn1
         >>> fn2
                 >>> fn3
                 <<< fn3
         <<< fn2
 <<< fn1

从上面可以看出,调用 next 实际上就是去调用下一个 中间件函数(也就是有些文章中所说的转移控制权)

下图摘自 koa 中文 官网 https://github.com/demopark/koa-docs-Zh-CN/blob/master/guide.md#%E7%BC%96%E5%86%99%E4%B8%AD%E9%97%B4%E4%BB%B6

img

代码

仓库在这里 https://github.com/koajs/compose 推荐本地拉下来仓库,进行手动断点理解 https://github.com/koajs/compose/blob/master/test/test.js

base case

下面是一个基本的测试 case

  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]))
  })
function compose(middleware) {
  return function (context, next) {
    return dispatch(0)
    function dispatch(i) {
      const fn = middleware[i]
      if (!fn) return Promise.resolve()
      return Promise.resolve(fn(context, dispatch.bind(null, i + 1)))
    }
  }
}
PicGo20230501160543

这段代码是 koa-compose 的核心逻辑,我删除了原有代码中的分支代码

compose 的输入是一个函数组成的数组 middleware,输出是一个接受 context【一个对象】 和 next 【一个函数】的函数

context 对象会在 middleware 中的所有函数中的第一个参数拿到 next 则是对下一个中间件函数的调用函数,对应代码的 dispatch.bind(null, i + 1)

重点说一下为啥要 return 其实如果我们传入的中间件中不会用到 Promise 或着 async await 的写法 不 return 也是 OK 的,因为函数调用本身就在转移控制

  it.only("should work in not return condition", async () => {
    const arr = []
    const stack = []

    stack.push((context, next) => {
      arr.push(1)
      next()
      arr.push(6)
    })

    stack.push((context, next) => {
      arr.push(2)
      next()
      arr.push(5)
    })

    stack.push((context, next) => {
      arr.push(3)
      next()
      arr.push(4)
    })

    await compose(stack)({})
    expect(arr).toEqual(expect.arrayContaining([1, 2, 3, 4, 5, 6]))
  })
function compose(middleware) {
  return function (context, next) {
    return dispatch(0)
    function dispatch(i) {
      const fn = middleware[i]
      if (!fn) return
      fn(context, dispatch.bind(null, i + 1))
    }
  }
}
PicGo20230501162208

但我们 should work case 就跑不通了

究其原因是,我们需要等待 next() 这个函数调用完,如果其中存在 Promise 的执行流,也要能让外界知道他的执行情况

所以必须要 return,我们调用 next 的时候,通常也会进行 await next() ,next 的参数和返回值都不关注,它的作用只是用来控制程序流

从结果上来看,只考虑主分支,会形成如下的效果

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();
                  })
              )
          })
        )
    })
  );
};

至于 return Promise.resolve(fn(context, dispatch.bind(null, i + 1)))Promise.resolve 包一层是否必要 如果我们只考虑 await next 的场景,不是必要的的,因为 参考

const a = await 1
// ->
let a
Promise.resolve(1).then(v => a = v)
PicGo20230501164043

只有这个 case 没通过

  it('should create next functions that return a Promise', function () {
    const stack = []
    const arr = []
    for (let i = 0; i < 5; i++) {
      stack.push((context, next) => {
        arr.push(next())
      })
    }

    compose(stack)({})

    for (const next of arr) {
      assert(isPromise(next), 'one of the functions next is not a Promise')
    }
  })

function isPromise (x) {
  return x && typeof x.then === 'function'
}

https://stackoverflow.com/questions/27746304/how-to-check-if-an-object-is-a-promise

If it has a .then function - that’s the only standard promise libraries use.

边界情况

看注释吧

/**
 - 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) {
      // 不能重复调用next
      if (i <= index) return Promise.reject(new Error("next() called multiple times"))
      index = i
      let fn = middleware[i]
      // 支持 compose([fn1, fn2, fn3])({}, fn4) 的写法,fn4 会在 fn3 后执行
      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([fn1, fn2, fn3])({}, fn4) 的写法,fn4 会在 fn3 后执行 这点 可以结合下面理解下

  it("should work 2", 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)({}, async (_, next) => {
      arr.push(99.1)
      await next()
      arr.push(99.2)
    })
    expect(arr).toEqual(expect.arrayContaining([1, 2, 3, 99.1, 99.2, 4, 5, 6]))
  })

参考

https://github.com/lxchuan12/koa-compose-analysis https://bytedance.feishu.cn/wiki/wikcnnAWRea37N3fBa8cQOlkP4N