Skip to content

中间件

1. 什么是中间件?想象一下你的应用有个“保安”

在 NestJS 中,中间件 是一个在路由处理函数(也就是你Controller里的方法)接收到请求之前被调用的函数。 把它想象成你家门口的保安或公司的前台。在访客(请求)见到你(路由处理函数)之前,保安会先进行一些检查和处理,比如:

  • 记录访客信息:记录下谁在什么时间来访。
  • 检查身份:确认访客是否有权限进入。
  • 指引路线:告诉访客应该去哪个办公室。

NestJS 的中间件扮演着类似的角色,它可以:

  • 执行任何代码。
  • 更改请求 (Request) 和响应 (Response) 对象。
  • 结束请求-响应周期(例如,如果验证失败,就直接返回错误信息,不让请求继续下去)。
  • 调用堆栈中的下一个中间件函数。

如果一个中间件没有结束请求-响应周期,它必须调用 next() 函数,将控制权传递给下一个中间件,否则请求将被挂起。

前置知识:请求-响应周期 (Request-Response Cycle)

为了更好地理解中间件,我们需要简单了解一下 Web 应用中最核心的流程之一:请求-响应周期

  1. 客户端发送请求:用户在浏览器中输入网址或点击一个按钮,浏览器就会向服务器发送一个 HTTP 请求。
  2. 服务器处理请求:服务器(你的 NestJS 应用)接收到这个请求。
  3. 中间件介入:在请求到达你编写的具体业务逻辑(例如,获取用户列表的函数)之前,中间件会先对请求进行处理。
  4. 路由处理:请求通过所有中间件后,会到达指定的路由处理函数。
  5. 服务器发送响应:路由处理函数执行完毕后,服务器会生成一个 HTTP 响应并将其发送回客户端。
  6. 客户端渲染响应:浏览器接收到响应并将其呈现给用户(例如,显示一个网页或一条成功消息)。

中间件正是在这个周期的第 3 步发挥作用,它像一道道关卡,对请求进行预处理。

2. 如何创建你的第一个中间件?

在 NestJS 中,你有两种方式来创建中间件:类(Class)函数(Function)

方式一:使用类 (Class-based Middleware) - 功能更强大

这是更常见和推荐的方式,因为它支持依赖注入 (Dependency Injection),我们稍后会解释。

一个基于类的中间件就是一个带有 @Injectable() 装饰器并实现了 NestMiddleware 接口的类。

让我们来创建一个简单的日志中间件,它会打印每个请求的方法和 URL。

src/logger.middleware.ts

typescript
import { Injectable, NestMiddleware } from '@nestjs/common';
import { Request, Response, NextFunction } from 'express';

@Injectable()
export class LoggerMiddleware implements NestMiddleware {
  use(req: Request, res: Response, next: NextFunction) {
    console.log(`[请求日志]... ${req.method} ${req.originalUrl}`);
    next(); // 非常重要!调用 next() 将请求传递给下一个处理程序
  }
}
  • @Injectable(): 这个装饰器告诉 NestJS,这个类是可被管理的,可以被注入到其他地方。
  • implements NestMiddleware: 实现这个接口可以确保你的类有正确的 use 方法结构。
  • use(req, res, next): 这是中间件的核心。req 是请求对象,res 是响应对象,next 是一个函数,用来调用下一个中间件。

方式二:使用函数 (Functional Middleware) - 简单快捷

如果你的中间件非常简单,不依赖任何其他服务,那么使用函数会更简洁。

src/logger.functional.middleware.ts

typescript
import { Request, Response, NextFunction } from 'express';

export function logger(req: Request, res: Response, next: NextFunction) {
  console.log(`[函数式中间件日志]... ${req.method} ${req.originalUrl}`);
  next();
}```
这个函数和类中间件里的 `use` 方法长得几乎一样,只是没有了类的包裹。

## 3. 如何应用中间件?

创建了中间件之后,我们需要告诉 NestJS 在哪里以及何时使用它。

### **应用在特定模块的特定路由中**

假设我们有一个 `CatsModule`,我们想让 `LoggerMiddleware` 只对 `cats` 相关的路由生效。

我们需要让 `CatsModule` 实现 `NestModule` 接口,并实现它的 `configure` 方法。

**`src/cats/cats.module.ts`**
```typescript
import { Module, NestModule, MiddlewareConsumer, RequestMethod } from '@nestjs/common';
import { CatsController } from './cats.controller';
import { CatsService } from './cats.service';
import { LoggerMiddleware } from '../logger.middleware'; // 引入中间件

@Module({
  controllers: [CatsController],
  providers: [CatsService],
})
export class CatsModule implements NestModule {
  configure(consumer: MiddlewareConsumer) {
    consumer
      .apply(LoggerMiddleware) // 应用中间件
      .forRoutes('cats'); // 指定对 'cats' 路由生效
      // 你也可以指定给某个 Controller
      // .forRoutes(CatsController);
      // 甚至可以更精确地指定某个请求方法
      // .forRoutes({ path: 'cats', method: RequestMethod.GET });
  }
}```

现在,每当有请求访问 `/cats` 路径时,控制台都会打印出我们的日志信息。

### **应用为全局中间件**

如果你希望中间件对应用中的**每一个**路由都生效,可以将它注册为全局中间件。这通常在 `main.ts` 文件中完成。

**`src/main.ts`**
```typescript
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { logger } from './logger.functional.middleware'; // 引入函数式中间件

async function bootstrap() {
  const app = await NestFactory.create(AppModule);

  // 将函数式中间件注册为全局中间件
  app.use(logger);

  await app.listen(3000);
}
bootstrap();

注意:通过 app.use() 注册的全局中间件无法使用依赖注入。 如果你的全局中间件需要依赖注入,你应该在 AppModule 中使用 .forRoutes('*') 的方式来应用它。

排除特定路由

有时候我们想让中间件对一个控制器下的所有路由生效,但又想排除其中几个。可以使用 .exclude() 方法。

typescript
// 在 AppModule 或其他模块的 configure 方法中
consumer
  .apply(LoggerMiddleware)
  .exclude(
    { path: 'cats', method: RequestMethod.GET }, // 排除 GET /cats
    { path: 'cats/breeds', method: RequestMethod.GET } // 排除 GET /cats/breeds
  )
  .forRoutes(CatsController); // 应用于 CatsController 下的所有其他路由

4. 进阶用法:依赖注入

这是类中间件相比函数中间件最大的优势。假设我们的日志中间件需要调用一个 LogService 来将日志记录到数据库或文件中。

src/log.service.ts

typescript
import { Injectable } from '@nestjs/common';

@Injectable()
export class LogService {
  log(message: string) {
    // 想象这里是写入数据库或文件的逻辑
    console.log(`[LogService]: ${message}`);
  }
}

src/advanced-logger.middleware.ts

typescript
import { Injectable, NestMiddleware } from '@nestjs/common';
import { Request, Response, NextFunction } from 'express';
import { LogService } from './log.service'; // 引入服务

@Injectable()
export class AdvancedLoggerMiddleware implements NestMiddleware {
  // 通过构造函数注入 LogService
  constructor(private readonly logService: LogService) {}

  use(req: Request, res: Response, next: NextFunction) {
    const message = `[高级日志]... ${req.method} ${req.originalUrl}`;
    this.logService.log(message); // 使用注入的服务
    next();
  }
}

为了让依赖注入生效,LogServiceAdvancedLoggerMiddleware 必须在同一个模块的作用域内被注册。

src/app.module.ts

typescript
import { Module, NestModule, MiddlewareConsumer } from '@nestjs/common';
import { AdvancedLoggerMiddleware } from './advanced-logger.middleware';
import { LogService } from './log.service'; // 确保服务被注册
import { AppController } from './app.controller';

@Module({
  imports: [],
  controllers: [AppController],
  providers: [LogService], // 在 providers 中注册 LogService
})
export class AppModule implements NestModule {
  configure(consumer: MiddlewareConsumer) {
    consumer
      .apply(AdvancedLoggerMiddleware)
      .forRoutes('*'); // 应用于所有路由
  }
}

5. 中间件 vs. 守卫 (Guards) vs. 拦截器 (Interceptors) vs. 管道 (Pipes)

对于初学者来说,很容易混淆这几个概念,因为它们都在处理请求的某个环节发挥作用。

简单来说,它们的职责和执行顺序如下:

执行顺序: 中间件 -> 守卫 -> 拦截器 (之前) -> 管道 -> 路由处理函数 -> 拦截器 (之后) -> 过滤器 (异常时)

组件主要职责执行时机典型用例
中间件 (Middleware)请求预处理,与框架无关的逻辑在路由处理函数之前日志记录、CORS、处理原始请求体
守卫 (Guards)授权 (Authorization)路由处理函数执行前检查用户角色、权限、JWT 令牌是否有效
拦截器 (Interceptors)AOP (面向切面编程),转换请求/响应路由处理函数执行前后转换响应数据格式、缓存响应、测量请求耗时
管道 (Pipes)转换验证路由处理函数参数传入前验证请求体 (DTO)、将字符串 ID 转换为数字
过滤器 (Filters)异常处理当发生未捕获的异常时格式化错误响应、记录错误日志

一句话总结

  • 中间件:最靠近原始请求的“门卫”,做一些基础处理。
  • 守卫:决定“你能不能进”的“保安”。
  • 拦截器:在“进门前后”给你“变个身”或“拍张照”的“魔术师”。
  • 管道:检查你带的“礼物”(数据)是否合格的“安检员”。
  • 过滤器:处理你搞出“乱子”(异常)的“清洁工”。

总结

恭喜你!现在你已经对 NestJS 的中间件有了全面的了解。我们从它是什么、如何创建,到如何应用以及更高级的依赖注入用法,都进行了详细的探讨。

记住,中间件是处理跨领域关注点(如日志、安全)的强大工具。 合理地使用它可以让你的代码更加清晰、模块化和易于维护。 现在,就在你的项目中动手尝试创建第一个中间件吧!