2、NestJS中间件基础:中间件概念、NestJS中间件分类(函数式、类式)、中间件执行顺序与生命周期
2.1 中间件概念
中间件(Middleware)是NestJS请求处理管道中的核心组件,它在路由处理器之前执行,负责对请求和响应对象进行预处理、转换或拦截。在S3接口错误处理与日志链路的场景中,中间件承担着“第一道关卡”的角色——在请求到达控制器之前完成日志注入、请求校验、错误捕获等通用逻辑。
核心职责: 中间件函数可以访问请求对象(req)、响应对象(res)以及next函数,通过调用next()将控制权传递给下一个中间件或路由处理器。
中间件在NestJS中主要解决以下三类问题:
- 日志链路初始化:为每个请求生成唯一traceId,注入请求上下文
- 请求预处理:解析请求头、校验Token、格式化请求体
- 错误边界:捕获同步/异步异常,统一返回错误响应
2.2 NestJS中间件分类
NestJS支持两种中间件实现方式:函数式中间件和类式中间件。两者在功能上等价,但在代码组织、依赖注入和可测试性方面存在差异。
| 对比维度 | 函数式中间件 | 类式中间件 |
|---|---|---|
| 定义方式 | 普通函数,接收(req, res, next) | 实现NestMiddleware接口的类 |
| 依赖注入 | 不支持(无法注入Service等) | 支持(通过构造函数注入) |
| 可测试性 | 较低(需手动模拟依赖) | 较高(可注入mock依赖) |
| 适用场景 | 简单逻辑:日志、CORS、静态校验 | 复杂逻辑:数据库查询、外部服务调用 |
| 代码复用 | 通过高阶函数实现 | 通过继承/组合实现 |
2.2.1 函数式中间件
函数式中间件是最简洁的实现方式,适用于不需要依赖注入的轻量级场景。在S3接口的日志链路中,我们常用函数式中间件快速注入traceId。
// logger.middleware.ts - 函数式中间件示例
import { Request, Response, NextFunction } from 'express';
import { v4 as uuidv4 } from 'uuid';
export function loggerMiddleware(req: Request, res: Response, next: NextFunction) {
// 为每个请求生成唯一traceId
const traceId = uuidv4();
req['traceId'] = traceId;
console.log(`[${traceId}] ${req.method} ${req.url}`);
// 记录响应耗时
const start = Date.now();
res.on('finish', () => {
const duration = Date.now() - start;
console.log(`[${traceId}] ${res.statusCode} ${duration}ms`);
});
next();
}
2.2.2 类式中间件
类式中间件通过实现NestMiddleware接口定义,支持依赖注入,适合需要访问数据库、配置服务或外部API的复杂场景。在S3错误处理中,我们使用类式中间件注入S3配置服务和错误日志服务。
// s3-error-handler.middleware.ts - 类式中间件示例
import { Injectable, NestMiddleware } from '@nestjs/common';
import { Request, Response, NextFunction } from 'express';
@Injectable()
export class S3ErrorHandlerMiddleware implements NestMiddleware {
constructor(private readonly s3ConfigService: S3ConfigService) {}
use(req: Request, res: Response, next: NextFunction) {
// 注入S3配置上下文
req['s3Config'] = this.s3ConfigService.getConfig();
// 捕获路由处理中的S3异常
const originalSend = res.send;
res.send = function (body: any) {
if (body?.error?.code?.startsWith('S3')) {
console.error(`S3 Error: ${body.error.code} - ${body.error.message}`);
// 统一格式化S3错误响应
body = {
status: 'error',
code: body.error.code,
message: 'S3服务异常,请稍后重试',
};
}
return originalSend.call(this, body);
};
next();
}
}
最佳实践: 在S3接口项目中,建议将日志链路初始化(traceId注入)使用函数式中间件,将S3错误处理(依赖配置服务)使用类式中间件,各司其职,保持代码清晰。
2.3 中间件执行顺序与生命周期
中间件的执行顺序遵循先进先出(FIFO)原则,即注册顺序决定了执行顺序。理解执行顺序对于正确设计S3接口的错误处理链路至关重要。
2.3.1 执行顺序规则
- 全局中间件(通过
app.use()注册)优先于模块中间件执行 - 模块中间件(通过
configure()注册)按模块导入顺序执行 - 路由中间件(通过
@UseMiddleware()装饰器)在模块中间件之后执行 - 同一层级多个中间件按注册顺序依次执行
2.3.2 生命周期关键节点
| 阶段 | 触发时机 | 典型操作 | 注意事项 |
|---|---|---|---|
| 初始化 | 应用启动时 | 中间件实例化、依赖注入 | 类式中间件在模块初始化时创建 |
| 请求进入 | 每次HTTP请求到达 | 执行use()方法逻辑 | 必须调用next()或返回响应 |
| next()调用 | 当前中间件处理完成 | 将控制权交给下一个中间件 | 不调用next()会导致请求挂起 |
| 异常抛出 | 中间件内部发生错误 | 跳过后续中间件,进入异常过滤器 | 使用try/catch捕获后调用next(error) |
| 响应返回 | 路由处理器或中间件调用res.send() | 响应数据返回客户端 | 中间件可拦截响应进行后处理 |
2.3.3 模块中注册中间件示例
// app.module.ts - 中间件注册示例
import { Module, NestModule, MiddlewareConsumer } from '@nestjs/common';
import { LoggerMiddleware } from './middleware/logger.middleware';
import { S3ErrorHandlerMiddleware } from './middleware/s3-error-handler.middleware';
@Module({
imports: [S3Module, ConfigModule],
})
export class AppModule implements NestModule {
configure(consumer: MiddlewareConsumer) {
consumer
.apply(LoggerMiddleware) // 先注册日志中间件
.forRoutes('*') // 应用于所有路由
consumer
.apply(S3ErrorHandlerMiddleware) // 后注册S3错误处理中间件
.forRoutes('s3/*'); // 仅应用于S3相关路由
}
}
常见陷阱: 中间件注册顺序决定了执行顺序。如果日志中间件注册在错误处理中间件之后,则错误处理中间件中发生的异常将无法被日志中间件捕获。在S3接口项目中,务必先注册日志中间件,再注册错误处理中间件,确保日志链路完整。
2.4 本章小结
- 中间件概念:请求处理管道中的预处理组件,在路由之前执行
- 函数式中间件:轻量、无依赖注入,适合日志、traceId注入等简单场景
- 类式中间件:支持依赖注入,适合S3配置、错误日志等复杂业务
- 执行顺序:全局 → 模块 → 路由,同一层级按注册顺序FIFO
- 生命周期:初始化 → 请求进入 → next()调用 → 异常处理 → 响应返回