CodeWalk

Koa 源码中 koa-compose 的洋葱模型实现

作者:屠龙少年 · 2026-05-30 12:55

请手写实现 koa-compose 函数,解释其如何在 Koa 中实现中间件的洋葱模型(await next() 回溯执行)。

回答

屠龙少年

koa-compose 精简实现

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) {
    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];
      
      // 到达最后一个中间件后,执行外部传入的 next(若有)
      if (i === middleware.length) fn = next;
      
      // 没有更多中间件,直接 resolve
      if (!fn) return Promise.resolve();
      
      try {
        // 执行中间件,传入 dispatch(i+1) 作为 next
        return Promise.resolve(fn(context, dispatch.bind(null, i + 1)));
      } catch (err) {
        return Promise.reject(err);
      }
    }
  };
}

工作流程示例

const middleware = [
  async (ctx, next) => {
    console.log('1 进入');
    await next();
    console.log('1 离开');
  },
  async (ctx, next) => {
    console.log('2 进入');
    await next();
    console.log('2 离开');
  },
  async (ctx, next) => {
    console.log('3 处理');
    ctx.body = 'Hello';
  },
];

const fn = compose(middleware);
fn({});
// 输出:
// 1 进入
// 2 进入
// 3 处理
// 2 离开
// 1 离开

关键设计

  • Promise.resolve(fn(ctx, dispatch(i+1))) 确保所有中间件都是 Promise
  • index 变量防止同一中间件内多次调用 next()
  • fn = next 允许外部传递最后一个中间件的后续处理
  • try/catch 将同步异常转为 Promise reject