3. 中间件设计原则:洋葱模型、职责链模式、中间件的生命周期、错误中间件的注册顺序

在 BFF 层构建错误处理机制,中间件架构是核心骨架。本章深入剖析中间件的四大设计原则:洋葱模型的执行流程、职责链模式的解耦思想、中间件生命周期的各个阶段,以及错误中间件注册顺序对错误捕获的直接影响。理解这些原则,是设计可穿透、可熔断的错误处理链路的前提。

3.1 洋葱模型:请求与响应的对称穿透

洋葱模型是 Node.js 框架(如 Koa、Express)中间件执行的核心模型。请求从外层中间件逐层向内穿透,到达核心处理器后,响应再逐层向外返回。每一层中间件都有机会在请求前和响应后执行逻辑,形成对称的“入栈-出栈”结构。

关键特征: 请求阶段(入栈)与响应阶段(出栈)的代码在同一个中间件函数内,以 await next() 或 next() 为分界线。
洋葱模型:请求穿透与响应返回 中间件 A(日志/计时) 请求 → 记录开始时间 | await next() | 计算耗时 → 响应 中间件 B(认证/鉴权) 请求 → 校验 Token | await next() | 注入用户信息 → 响应 中间件 C(参数校验/转换) 请求 → 校验 Body | await next() | 格式化输出 → 响应 核心处理器(Controller) 执行业务逻辑,返回响应数据 请求入栈 响应出栈 每个中间件在 await next() 之前处理请求,之后处理响应;错误中间件可在此链中任意位置捕获异常。
实践要点: 洋葱模型要求中间件严格按照注册顺序执行。先注册的中间件在外层,后注册的在内层。错误中间件通常注册在链路最外层,以确保能捕获所有内层抛出的异常。

3.2 职责链模式:解耦与灵活编排

职责链模式将多个处理节点串联成一条链,每个节点决定是否处理请求,或将请求传递给下一个节点。在 BFF 中间件中,每个中间件就是一个职责节点,通过 next() 实现链式传递。这种模式带来了三大优势:

  • 解耦性: 每个中间件只关注单一职责(日志、认证、限流、错误处理),互不依赖。
  • 可编排性: 通过调整注册顺序,可以灵活组合不同的处理链路,适应不同路由或版本的需求。
  • 可扩展性: 新增中间件只需插入链中,无需修改现有代码。
职责节点 职责描述 是否阻断链
错误捕获中间件 捕获所有未处理的异常,返回统一错误响应 是(终止链)
请求日志中间件 记录请求方法、URL、耗时 否(继续传递)
认证中间件 校验 Token,注入用户信息 是(认证失败时阻断)
限流中间件 检查请求频率,超出阈值则拒绝 是(超限时阻断)
参数校验中间件 校验请求体/查询参数格式 是(校验失败时阻断)
业务控制器 执行核心业务逻辑 是(返回响应)
避坑提示: 职责链中如果某个中间件忘记调用 next(),且没有返回响应,请求将永久挂起,导致客户端超时。务必在中间件末尾显式调用 next() 或返回响应。

3.3 中间件的生命周期

一个中间件从注册到销毁,经历四个阶段。理解生命周期有助于在正确的时间点执行初始化、清理和错误处理逻辑。

  1. 注册阶段: 中间件被添加到应用实例的中间件队列中。此时尚未处理任何请求,适合执行一次性初始化(如建立数据库连接、加载配置)。
  2. 请求入栈阶段: 请求到达,中间件按注册顺序依次执行 next() 之前的代码。此阶段适合做前置处理(日志、认证、限流)。
  3. 响应出栈阶段: 内层中间件和控制器执行完毕后,响应逐层返回。中间件执行 next() 之后的代码,适合做后置处理(耗时计算、响应格式化、错误包装)。
  4. 销毁阶段: 应用关闭或中间件被移除时触发。适合释放资源(关闭连接、清除定时器)。
中间件生命周期四阶段 ① 注册阶段 app.use(middleware) ② 请求入栈 前置处理逻辑 ③ 响应出栈 后置处理逻辑 ④ 销毁阶段 资源释放 生命周期关键行为 • 注册阶段:适合做一次性初始化,如连接池、配置加载。 • 入栈阶段:执行 await next() 之前的代码,可阻断请求。 • 出栈阶段:执行 await next() 之后的代码,可捕获内层错误。 • 销毁阶段:应用关闭时自动触发,确保资源正确释放。

3.4 错误中间件的注册顺序

错误中间件是 BFF 错误处理的核心防线。它的注册顺序直接决定了错误能否被正确捕获。错误中间件通常有四个参数 (err, req, res, next),与普通中间件 (req, res, next) 区分。

核心规则: 错误中间件必须注册在所有可能抛出错误的中间件之后,且通常放在中间件链的最外层(即最先注册的位置),以确保能捕获整个链路的异常。

以下是一个典型的错误中间件注册顺序示例:

// 1. 最外层:全局错误捕获中间件(最先注册)
app.use(async (err, req, res, next) => {
  console.error('Unhandled error:', err);
  res.status(err.status || 500).json({
    code: err.code || 'INTERNAL_ERROR',
    message: err.message || 'Internal Server Error'
  });
});

// 2. 请求日志中间件
app.use(async (req, res, next) => {
  const start = Date.now();
  await next();
  console.log(`${req.method} ${req.url} - ${Date.now() - start}ms`);
});

// 3. 认证中间件
app.use(async (req, res, next) => {
  const token = req.headers.authorization;
  if (!token) {
    // 抛出错误,会被外层错误中间件捕获
    throw new AuthError('Missing token');
  }
  req.user = await verifyToken(token);
  await next();
});

// 4. 限流中间件
app.use(rateLimiter);

// 5. 参数校验中间件
app.use(validationMiddleware);

// 6. 业务控制器(最内层)
app.use('/api/orders', orderController);
注册位置 中间件类型 错误捕获范围 典型用途
最外层(最先注册) 全局错误中间件 捕获所有内层中间件和控制器抛出的错误 统一错误响应格式、日志记录、告警触发
中间层 局部错误中间件 捕获特定路由或中间件组的错误 针对特定模块做错误转换或降级处理
最内层(最后注册) 业务控制器 不捕获错误,只抛出业务异常 执行业务逻辑,抛出 BusinessError
常见错误: 如果将错误中间件注册在业务控制器之后,控制器抛出的错误将无法被该错误中间件捕获,因为请求在控制器处已经返回,不会继续向后传递。错误中间件必须注册在可能抛出错误的中间件之前(从洋葱模型角度看是外层)。

3.5 错误中间件的穿透与熔断设计

在 BFF 层,错误中间件不仅要捕获错误,还要实现穿透和熔断两种机制:

  • 穿透: 错误中间件捕获异常后,可以选择不终止请求,而是将错误信息包装后继续传递给下一个中间件(通过 next(err)),实现错误信息的逐层传递和增强。
  • 熔断: 当错误率达到阈值时,错误中间件可以触发熔断,直接返回降级响应,避免请求穿透到后端服务,保护下游系统。
// 错误穿透示例:逐层增强错误信息
// 内层错误中间件
app.use('/api/orders', async (err, req, res, next) => {
  // 增强错误上下文
  err.context = err.context || {};
  err.context.route = '/api/orders';
  err.context.timestamp = Date.now();
  next(err); // 穿透到外层错误中间件
});

// 外层全局错误中间件
app.use(async (err, req, res, next) => {
  // 最终处理:记录日志并返回响应
  logger.error({ err, context: err.context });
  res.status(err.status || 500).json({
    error: err.message,
    requestId: req.id
  });
});
设计建议: 在 BFF 层,建议将错误中间件分为两层:内层错误中间件负责错误增强和局部降级,外层全局错误中间件负责统一响应和告警。这种分层设计既保证了错误信息的完整性,又实现了关注点分离。