每日一个npm包 —— koa-compose

733 阅读3分钟

介绍

💡 koa-compose 将多个中间件合并成一个大的中间件。调用这个大的中间件就可以依次执行其内多个小中间件,从而完成一系列任务。

正是 koa-compose 将多个中间件组合才形成了 koa洋葱模型

image.png

koa 洋葱模型

源码分析

下载 koa-compose 发现其逻辑只存在于 index.js 文件中,文件只有 48 行,导出一个 compose 函数。它接收一个需要合成的中间件数组,返回一个新的中间件。

compose 函数先是对入参 middleware 进行校验,判断其是否为函数数组,如果不是则抛出错误。如果是,通过校验,返回一个函数,这个函数就是新生成的中间件。

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!')
  }

  return function (context, next) {
    ...
  }
}

以下为 compose 返回的中间件的具体逻辑:

  1. 定义 indexmiddleware[index] 为上一次被调用的中间件,由于最开始没有中间件被调用,所以初始化为 -1
  2. 定义 dispatch 函数:其入参 i,内部将调用 middleware[i] 中间件
  3. 执行 dispatch(0),开始逐一调用中间件。
function compose (middleware) {
  ...

  return function (context, next) {
    // 1
    let index = -1

    // 2
    function dispatch (i) {
      ...
    }

    // 3
    return dispatch(0)
  }
}

dispatch 函数

dispatch 函数是最精妙的部分,其内部会调用中间件 middleware[i],并通过巧妙入参,使得这个中间件具有调用下一中间件的能力。

其核心逻辑简化为:

function dispatch (i) {
  const fn = middleware[i]  // eg: fn = loggerMiddleware

  fn(context, dispatch.bind(null, i + 1))
}

function loggerMiddleware(context, next) {
  // ... -> 调用下一中间件 `前` 的逻辑

  next()  // 调用下一中间件

  // ... -> 调用下一中间件 `后` 的逻辑
}

可以看到 dispatch 在调用 middleware[i] 的时候向其传入的第二个参数 nextdispatch.bind(null, i + 1),中间件在调用 next() 时,等同调用 dispatch(i + 1),即调用下一中间件。

dispatch 完整逻辑如下:

  1. 判断当前中间件是否被执行过。

    比较 iindex 大小:

    • middleware[i] 为本次需要调用的中间件。
    • middleware[index] 为上次调用的中间件。

    i <= index 表示已经调用过 middleware[i] 后面的中间件。中间件是顺序执行的,所以 middleware[i] 曾经也被调用过。前文知道 middleware[i]dispatch(i) 调用的,而 dispatch() 是开发者通过 next() 调用,所以抛出错误 new Error('next() called multiple times')

    若当前中间件 middleware[i] 是第一次调用,则通过校验,同时更新 index = i

  2. 确定要调用的中间件 fn

    正常情况下 fn = middleware[i],但还记得前文说开发者写的中间件逻辑中会调用 next() 来调用下一中间件吗?若当前中间件是 middleware 数组中最后一个中间件怎么办?(最后一个中间件是通过 dispatch(middleware.length - 1) 来调用,其内部调用 next() 时,相当于调用 dispatch(middleware.length) ) if (i === middleware.length) fn = next 就是来应对这个 case 的,这时 fn 被设置为 next,即 compose 返回中间件的第二个入参。(其实还有 i === middleware.length + 1 的情况,读者可以思考一下~)

  3. 调用中间件 fn

    如果 fn 不存在(next === undefined || i === middleware.length + 1),则返回空的 resolve 对象。

    如果 fn 存在,则调用 fn,并入参 next = dispatch.bind(null, i + 1)

function compose (middleware) {
  ...

  return function (context, next) {
    let index = -1

    function dispatch (i) {
      // 1
      if (i <= index) return Promise.reject(new Error('next() called multiple times'))
      index = i

      // 2
      let fn = middleware[i]
      if (i === middleware.length) fn = next

      // 3
      if (!fn) return Promise.resolve()
      try {
        return Promise.resolve(fn(context, dispatch.bind(null, i + 1)))
      } catch (err) {
        return Promise.reject(err)
      }
    }

    return dispatch(0)
  }
}

总结

koa-compose 利用函数式编程 compose 的思想,接受一个中间件数组,返回一个新的中间件。其代码很短,除去注释信息只有 23 行,设计也很精妙。它使得被合成的中间件通过调用 next(),从而对下一个中间件进行调用,而不用考虑多个中间件是如何连接在一起的。建议读者自己动手翻翻源码。可以对函数式编程 compose 思想和闭包有更清晰的认识。