3. 中间件设计原则:洋葱模型、职责链模式、中间件的生命周期、错误中间件的注册顺序
在 BFF 层构建错误处理机制,中间件架构是核心骨架。本章深入剖析中间件的四大设计原则:洋葱模型的执行流程、职责链模式的解耦思想、中间件生命周期的各个阶段,以及错误中间件注册顺序对错误捕获的直接影响。理解这些原则,是设计可穿透、可熔断的错误处理链路的前提。
3.1 洋葱模型:请求与响应的对称穿透
洋葱模型是 Node.js 框架(如 Koa、Express)中间件执行的核心模型。请求从外层中间件逐层向内穿透,到达核心处理器后,响应再逐层向外返回。每一层中间件都有机会在请求前和响应后执行逻辑,形成对称的“入栈-出栈”结构。
关键特征: 请求阶段(入栈)与响应阶段(出栈)的代码在同一个中间件函数内,以
await next() 或 next() 为分界线。
实践要点: 洋葱模型要求中间件严格按照注册顺序执行。先注册的中间件在外层,后注册的在内层。错误中间件通常注册在链路最外层,以确保能捕获所有内层抛出的异常。
3.2 职责链模式:解耦与灵活编排
职责链模式将多个处理节点串联成一条链,每个节点决定是否处理请求,或将请求传递给下一个节点。在 BFF 中间件中,每个中间件就是一个职责节点,通过 next() 实现链式传递。这种模式带来了三大优势:
- 解耦性: 每个中间件只关注单一职责(日志、认证、限流、错误处理),互不依赖。
- 可编排性: 通过调整注册顺序,可以灵活组合不同的处理链路,适应不同路由或版本的需求。
- 可扩展性: 新增中间件只需插入链中,无需修改现有代码。
| 职责节点 | 职责描述 | 是否阻断链 |
|---|---|---|
| 错误捕获中间件 | 捕获所有未处理的异常,返回统一错误响应 | 是(终止链) |
| 请求日志中间件 | 记录请求方法、URL、耗时 | 否(继续传递) |
| 认证中间件 | 校验 Token,注入用户信息 | 是(认证失败时阻断) |
| 限流中间件 | 检查请求频率,超出阈值则拒绝 | 是(超限时阻断) |
| 参数校验中间件 | 校验请求体/查询参数格式 | 是(校验失败时阻断) |
| 业务控制器 | 执行核心业务逻辑 | 是(返回响应) |
避坑提示: 职责链中如果某个中间件忘记调用
next(),且没有返回响应,请求将永久挂起,导致客户端超时。务必在中间件末尾显式调用 next() 或返回响应。
3.3 中间件的生命周期
一个中间件从注册到销毁,经历四个阶段。理解生命周期有助于在正确的时间点执行初始化、清理和错误处理逻辑。
- 注册阶段: 中间件被添加到应用实例的中间件队列中。此时尚未处理任何请求,适合执行一次性初始化(如建立数据库连接、加载配置)。
- 请求入栈阶段: 请求到达,中间件按注册顺序依次执行
next()之前的代码。此阶段适合做前置处理(日志、认证、限流)。 - 响应出栈阶段: 内层中间件和控制器执行完毕后,响应逐层返回。中间件执行
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 层,建议将错误中间件分为两层:内层错误中间件负责错误增强和局部降级,外层全局错误中间件负责统一响应和告警。这种分层设计既保证了错误信息的完整性,又实现了关注点分离。