中间件
1. 什么是中间件?想象一下你的应用有个“保安”
在 NestJS 中,中间件 是一个在路由处理函数(也就是你Controller
里的方法)接收到请求之前被调用的函数。 把它想象成你家门口的保安或公司的前台。在访客(请求)见到你(路由处理函数)之前,保安会先进行一些检查和处理,比如:
- 记录访客信息:记录下谁在什么时间来访。
- 检查身份:确认访客是否有权限进入。
- 指引路线:告诉访客应该去哪个办公室。
NestJS 的中间件扮演着类似的角色,它可以:
- 执行任何代码。
- 更改请求 (Request) 和响应 (Response) 对象。
- 结束请求-响应周期(例如,如果验证失败,就直接返回错误信息,不让请求继续下去)。
- 调用堆栈中的下一个中间件函数。
如果一个中间件没有结束请求-响应周期,它必须调用 next()
函数,将控制权传递给下一个中间件,否则请求将被挂起。
前置知识:请求-响应周期 (Request-Response Cycle)
为了更好地理解中间件,我们需要简单了解一下 Web 应用中最核心的流程之一:请求-响应周期。
- 客户端发送请求:用户在浏览器中输入网址或点击一个按钮,浏览器就会向服务器发送一个 HTTP 请求。
- 服务器处理请求:服务器(你的 NestJS 应用)接收到这个请求。
- 中间件介入:在请求到达你编写的具体业务逻辑(例如,获取用户列表的函数)之前,中间件会先对请求进行处理。
- 路由处理:请求通过所有中间件后,会到达指定的路由处理函数。
- 服务器发送响应:路由处理函数执行完毕后,服务器会生成一个 HTTP 响应并将其发送回客户端。
- 客户端渲染响应:浏览器接收到响应并将其呈现给用户(例如,显示一个网页或一条成功消息)。
中间件正是在这个周期的第 3 步发挥作用,它像一道道关卡,对请求进行预处理。
2. 如何创建你的第一个中间件?
在 NestJS 中,你有两种方式来创建中间件:类(Class) 和 函数(Function)。
方式一:使用类 (Class-based Middleware) - 功能更强大
这是更常见和推荐的方式,因为它支持依赖注入 (Dependency Injection),我们稍后会解释。
一个基于类的中间件就是一个带有 @Injectable()
装饰器并实现了 NestMiddleware
接口的类。
让我们来创建一个简单的日志中间件,它会打印每个请求的方法和 URL。
src/logger.middleware.ts
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
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()
方法。
// 在 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
import { Injectable } from '@nestjs/common';
@Injectable()
export class LogService {
log(message: string) {
// 想象这里是写入数据库或文件的逻辑
console.log(`[LogService]: ${message}`);
}
}
src/advanced-logger.middleware.ts
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();
}
}
为了让依赖注入生效,LogService
和 AdvancedLoggerMiddleware
必须在同一个模块的作用域内被注册。
src/app.module.ts
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 的中间件有了全面的了解。我们从它是什么、如何创建,到如何应用以及更高级的依赖注入用法,都进行了详细的探讨。
记住,中间件是处理跨领域关注点(如日志、安全)的强大工具。 合理地使用它可以让你的代码更加清晰、模块化和易于维护。 现在,就在你的项目中动手尝试创建第一个中间件吧!