实现中间件机制

手写 Koa 风格的中间件机制,理解洋葱模型

问题

实现一个简单的中间件机制(Koa style),支持洋葱模型和异步中间件。

解答

中间件组合函数 compose

function compose(middlewares) {
  // 返回一个接收 context 的函数
  return function (ctx) {
    // 从第一个中间件开始执行
    return dispatch(0);

    function dispatch(i) {
      // 取出当前中间件
      const fn = middlewares[i];

      // 所有中间件执行完毕
      if (!fn) {
        return Promise.resolve();
      }

      try {
        // 执行中间件,传入 ctx 和 next 函数
        // next 函数就是执行下一个中间件
        return Promise.resolve(fn(ctx, () => dispatch(i + 1)));
      } catch (err) {
        return Promise.reject(err);
      }
    }
  };
}

简易 Koa 实现

class Koa {
  constructor() {
    this.middlewares = [];
  }

  // 添加中间件
  use(fn) {
    this.middlewares.push(fn);
    return this; // 支持链式调用
  }

  // 组合并执行所有中间件
  callback() {
    const fn = compose(this.middlewares);
    return (ctx) => fn(ctx);
  }
}

测试洋葱模型

const app = new Koa();

// 中间件 1
app.use(async (ctx, next) => {
  console.log('1 - 进入');
  ctx.body = '1';
  await next();
  console.log('1 - 离开');
});

// 中间件 2
app.use(async (ctx, next) => {
  console.log('2 - 进入');
  ctx.body += '2';
  await next();
  console.log('2 - 离开');
});

// 中间件 3
app.use(async (ctx, next) => {
  console.log('3 - 进入');
  ctx.body += '3';
  await next();
  console.log('3 - 离开');
});

// 执行
const ctx = { body: '' };
app.callback()(ctx).then(() => {
  console.log('结果:', ctx.body);
});

// 输出:
// 1 - 进入
// 2 - 进入
// 3 - 进入
// 3 - 离开
// 2 - 离开
// 1 - 离开
// 结果: 123

异步中间件示例

const app = new Koa();

app.use(async (ctx, next) => {
  const start = Date.now();
  await next();
  console.log(`耗时: ${Date.now() - start}ms`);
});

app.use(async (ctx, next) => {
  // 模拟异步操作
  await new Promise((resolve) => setTimeout(resolve, 100));
  ctx.body = 'Hello';
  await next();
});

const ctx = {};
app.callback()(ctx).then(() => {
  console.log(ctx.body); // Hello
});
// 输出: 耗时: 100ms

关键点

  • 洋葱模型:中间件按顺序进入,逆序离开,形成洋葱状的执行流程
  • next 函数:调用 next() 会暂停当前中间件,执行下一个,完成后再返回
  • Promise 包装:用 Promise.resolve 包装确保同步和异步中间件都能正确处理
  • 递归调度:dispatch 函数通过递归实现中间件的串联执行
  • ctx 共享:所有中间件共享同一个 context 对象,可以传递数据