手写支持异步的 Koa 洋葱模型 compose,并防止多次 next。

先理解原理和示例,再练习完整口述与追问。

递归 dispatch;Promise 采纳;每次请求独立游标;同步异常转拒绝;next 只前进一次;中间件要 await/return。

同步函数 compose 把值从一步传给下一步,洋葱组合却把“执行下游的能力”作为 next 传入当前中间件。await next() 暂停当前后半段,等下游完成后恢复,因此能反向退出。每次执行创建自己的 last 游标,拒绝对已执行层级的二次 dispatch,避免一个中间件重复 next 导致响应和副作用执行多次。

js
function composeMiddleware(middlewares) {
  if (!Array.isArray(middlewares) || !middlewares.every(fn => typeof fn === 'function')) {
    throw new TypeError('中间件必须是函数数组')
  }
  return function run(context) {
    let last = -1
    function dispatch(index) {
      if (index <= last) return Promise.reject(new Error('next 不能重复调用'))
      last = index
      const middleware = middlewares[index]
      if (!middleware) return Promise.resolve()
      try {
        return Promise.resolve(middleware(context, () => dispatch(index + 1)))
      } catch (error) {
        return Promise.reject(error)
      }
    }
    return dispatch(0)
  }
}

A 写 A1、await next、写 A2,B 写 B1、await next、写 B2,执行得到 A1 B1 B2 A2。单次遍历额外空间随链深度 O(n),中间件自身工作另计;大量同步 next 嵌套仍要留意栈限制。

示例不包含 Koa 完整 ctx、响应和外部 next 协议。中间件忘记 await/return 下游时,组合器无法替调用方恢复正确的等待与错误捕获。

我用 dispatch 把下游封成 next,统一用 Promise 采纳同步和异步结果,当前层等待后形成反向返回。每次执行独立记录已到层级,阻止重复 next,并测试顺序、异常和并发请求隔离。