让请求“排队”又“转身”:手写一个轻量中间件的实践之旅


让请求“排队”又“转身”:手写一个轻量中间件的实践之旅

在现代 Web 开发中,“中间件”这个词常被反复提及——它像一条看不见的流水线,串起身份校验、日志记录、错误处理、数据转换等各类横切逻辑。但对初学者而言,中间件常被抽象为框架内置的黑盒:调用 app.use()、写个函数、next() 一扔,就完事了。可它究竟是如何工作的?如果脱离 Express 或 Koa,我们能否从零构建一个真正可理解、可调试、可复用的中间件机制?

答案是肯定的。实现一个简单而典型的中间件系统,核心不在语法糖,而在三个关键设计:函数组合、执行顺序控制、上下文共享

首先,明确中间件的本质:它是一组具有统一签名的函数,接收请求上下文(context)、可选的 next 函数(用于移交控制权),并可能修改 context 或提前终止流程。例如:

const logger = (ctx, next) => {
  console.log(`→ ${new Date().toISOString()} ${ctx.method} ${ctx.path}`);
  next();
};

const auth = (ctx, next) => {
  if (!ctx.headers?.authorization) {
    ctx.status = 401;
    ctx.body = { error: 'Unauthorized' };
    return; // 提前结束,不调用 next
  }
  next();
};

这里,ctx 是贯穿整个请求生命周期的共享对象,next 则是触发下一个中间件的“开关”。但单个函数无意义——真正的力量在于链式调度

我们来手写一个 compose 函数,它接收中间件数组,返回一个可执行的“合成函数”:

function compose(middlewares) {
  return function(ctx) {
    let index = -1;
    function dispatch(i) {
      if (i <= index) {
        return Promise.reject(new Error('next() called multiple times'));
      }
      index = i;
      const fn = middlewares[i];
      if (!fn) return Promise.resolve();
      try {
        return Promise.resolve(fn(ctx, () => dispatch(i + 1)));
      } catch (err) {
        return Promise.reject(err);
      }
    }
    return dispatch(0);
  };
}

这段代码看似简短,却承载了中间件运行的核心逻辑:

随机图片

  • dispatch(i) 递归调用自身,确保每个中间件仅执行一次;
  • next() 实际就是 () => dispatch(i + 1),将控制权交予下一个;
  • 使用 Promise.resolve() 统一异步处理,兼容同步与 async/await 中间件;
  • index 防止重复调用 next(),避免逻辑错乱。

有了 compose,我们就能组装中间件链:

const app = compose([logger, auth, (ctx) => {
  ctx.body = { message: 'Hello from middleware!' };
  ctx.status = 200;
}]);

// 模拟一次请求
const ctx = { method: 'GET', path: '/api', headers: {} };
app(ctx).then(() => {
  console.log('Response:', ctx.status, ctx.body);
});

运行时,控制台会先输出日志,再因缺少 authorization 头而返回 401——整个流程清晰可见,无需依赖任何框架。

这个简易中间件系统虽小,却已具备生产级中间件的骨架:
可插拔:任意符合 (ctx, next) => void | Promise 签名的函数均可加入;
可中断:通过不调用 next(),实现权限拦截、重定向等短路逻辑;
上下文透传:所有中间件共享同一 ctx,便于注入用户信息、数据库连接等;
错误冒泡try/catchPromise.reject 结合,使错误能逐层向上被捕获或统一处理。

当然,真实项目中还需补充超时控制、异常兜底、类型约束(如 TypeScript 接口定义)等能力。但这些优化,都建立在对“函数如何串联”这一本质的理解之上。

写到这里,或许你会意识到:所谓中间件,并非框架的恩赐,而是开发者对“关注点分离”这一古老原则的优雅践行——它把业务主干和通用逻辑解耦,让每个函数只专注一件事,再由一个精巧的调度器将其编织成网。

下一次你调用 app.use() 时,不妨暂停一秒,在心里默念那句 dispatch(0)。那不是魔法,而是你亲手写下的、最朴素也最有力的控制流。

发表评论