2、NestJS中间件基础:中间件概念、NestJS中间件分类(函数式、类式)、中间件执行顺序与生命周期

2.1 中间件概念

中间件(Middleware)是NestJS请求处理管道中的核心组件,它在路由处理器之前执行,负责对请求和响应对象进行预处理、转换或拦截。在S3接口错误处理与日志链路的场景中,中间件承担着“第一道关卡”的角色——在请求到达控制器之前完成日志注入、请求校验、错误捕获等通用逻辑。

核心职责: 中间件函数可以访问请求对象(req)、响应对象(res)以及next函数,通过调用next()将控制权传递给下一个中间件或路由处理器。

中间件在NestJS中主要解决以下三类问题:

  • 日志链路初始化:为每个请求生成唯一traceId,注入请求上下文
  • 请求预处理:解析请求头、校验Token、格式化请求体
  • 错误边界:捕获同步/异步异常,统一返回错误响应
NestJS请求管道:中间件执行位置 客户端 中间件层 日志注入 · 错误捕获 请求预处理 路由处理器 业务逻辑处理 S3接口调用 响应返回 💡 中间件执行时机 请求进入 → 中间件处理 → 路由处理器 → 响应返回 中间件在路由之前执行,可提前终止请求或注入上下文

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()装饰器)在模块中间件之后执行
  • 同一层级多个中间件按注册顺序依次执行
中间件执行顺序与生命周期 全局中间件 app.use() 注册 模块中间件 configure() 注册 路由中间件 @UseMiddleware() 路由处理器 业务逻辑 ⏱ 中间件生命周期 请求进入 → 全局中间件(1) → 模块中间件(2) → 路由中间件(3) → 路由处理器(4) → 响应返回 每个中间件调用 next() 后进入下一个,若调用 res.send() 则提前终止管道 ⚠️ 异常处理链路 中间件中抛出异常 → 跳过后续中间件 → 直接进入异常过滤器 → 返回错误响应

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()调用 → 异常处理 → 响应返回