背景与问题界定

中间件(Middleware)是 Node.js Web 框架最核心的架构模式。从 Express 的线性链式调用到 Koa 的洋葱模型(Onion Model),中间件的组织方式深刻影响着应用的执行流程、错误处理和扩展能力。许多开发者在日常使用中只是简单地调用 app.use(fn),却没有深入理解中间件的执行顺序、异步模型和错误传播机制。由此导致的问题包括:静态文件中间件与路由中间件的顺序颠导致 404 错误;错误处理中间件未正确放置导致异常被吞没;在 Koa 中使用 async/await 但漏写 next 导致的执行链断裂。理解并设计一套健壮的中间件体系,是构建稳定 Node.js 服务的基础。

目标拆解与工程约束

  • 中间件顺序决定了执行流程:请求处理链的顺序由中间件注册顺序决定,任何顺序错误都可能导致功能异常,必须提供清晰的顺序规范文档。
  • 错误处理中间件必须为最后一个中间件:Express 中错误处理中间件有 4 个参数 (err, req, res, next);Koa 中通过在 catch 中捕获,两者均要求错误处理在最后注册。
  • 异步中间件的正确率处理:async 中间件函数中的未捕获异常必须通过框架的错误处理渠道传播,不应使用全局的 process.on('unhandledRejection') 兜底。
  • 中间件的生命周期与资源清理:中间件可能分配的资源(数据库连接、文件句柄、计时器)应在中间件执行完毕或请求结束时释放,避免资源泄漏。

方案设计

Express 的中间件体系是线性的:请求按注册顺序依次通过每个中间件,遇到响应终止则停止传递。这种模型简单直观,但存在两个局限:一是无法在响应发出后执行后置逻辑(如记录响应时间);二是错误处理只能通过跳到 4 参数的错误中间件来处理。

Koa 的洋葱模型通过 async/awaitnext 函数实现了双向的执行流程。请求从外层中间件进入,经过 await next() 到达内层中间件,响应从内层逐步返回到外层:

// Koa 洋葱模型示意图
app.use(async (ctx, next) => {
  console.log('① 进入外层中间件')
  await next()
  console.log('⑥ 返回外层中间件 - 响应后置处理')
})

app.use(async (ctx, next) => {
  console.log('② 进入中层中间件')
  await next()
  console.log('⑤ 返回中层中间件')
})

app.use(async (ctx) => {
  console.log('③ 进入内层中间件(路由处理)')
  ctx.body = { message: 'Hello' }
  console.log('④ 返回内层中间件')
})
// 输出顺序: ① ② ③ ④ ⑤ ⑥

这种模型的优势在于:请求前和响应后的逻辑可以自然地写在 await next() 上下两侧。日志记录、性能计时、事务管理等功能只需要一个中间件即可覆盖请求全生命周期。

我们实现一个轻量级的中间件组合函数来理解其核心原理:

function compose(middlewares: Middleware[]) {
  return (context: any, next?: Middleware) => {
    let index = -1
    return dispatch(0)
    function dispatch(i: number): Promise<any> {
      if (i <= index) throw new Error('next() called multiple times')
      index = i
      let fn = middlewares[i]
      if (i === middlewares.length) fn = next
      if (!fn) return Promise.resolve()
      try {
        return Promise.resolve(fn(context, dispatch.bind(null, i + 1)))
      } catch (err) {
        return Promise.reject(err)
      }
    }
  }
}

对于错误处理,Koa 的错误处理也是在中间件中完成的——在外层使用 try-catch 包裹 await next(),捕获到错误后设置 ctx.statusctx.body。这种方式的优势是错误处理逻辑和正常逻辑在同一个中间件中,上下文的可见性更好。

实施路径与关键决策

  • 选择 Koa 作为新项目默认框架:Koa 的洋葱模型 + async/await 在复杂场景下更可控,Express 适用于对中间件生态有更强依赖的场景。
  • 制定中间件注册顺序规范:第一层:错误处理(兜底);第二层:安全相关(CORS、XSS、限流);第三层:公共功能(解析、日志);第四层:业务路由。
  • 中间件内部避免长同步操作:所有耗时操作通过异步接口执行,避免在 Lambda 闭包中捕获大闭包变量导致内存泄漏。
  • 提供中间件开发模板:规范化中间件的入参、出参和错误处理模式,便于团队快速产出符合标准的中间件组件。

验证指标与可持续迭代

通过中间件的执行链路追踪验证洋葱模型的完整性:使用 async_hookscls-hooked 建立请求级别的上下文,在每个中间件入口和出口打点,确保所有请求按正确顺序通过。错误处理中间件应捕获所有未处理的异常,错误日志包含完整的中间件调用栈。后续迭代方向包括:基于中间件的链路追踪(OpenTelemetry 集成)和请求/响应体的深度校验中间件。

工程落地思考

Express 和 Koa 的中间件设计分别代表了两种架构哲学的权衡。Express 选择简单直接——中间件是一个接一个的函数调用,易于理解和调试。Koa 则选择了优雅和强大——通过洋葱模型实现前置和后置逻辑的自然组合。在实际项目中,这两种模型各有适用场景,选择的标准不应该是"哪个更先进",而是"哪个更匹配团队的认知模型"。对于一个以 Node.js 为核心的后端团队来说,深入理解中间件的组合原理比选择哪个框架更重要,因为分布式系统中的网关层、消息队列消费者和 RPC 链路的中间件本质上都是同样的模式。