这一篇,我们将深入学习 拦截器(Interceptor),这是 NestJS 中处理请求响应的强大工具。
拦截器基于 RxJS Observable,可以在方法执行前后添加额外逻辑,非常适合用于响应转换、日志记录、缓存等场景。如果你熟悉前端的 Axios 拦截器或 Vue 的导航守卫,那么理解 NestJS 拦截器会非常轻松 🚀
📌 版本信息
本课程使用的 NestJS 版本是 11.0.1,需要 Node.js 20.x 或更高版本。
让我们开始探索拦截器的世界吧!
一、什么是拦截器?
拦截器的定义 🎯
拦截器(Interceptor) 是在方法执行前后添加额外逻辑的类。它实现了 NestInterceptor 接口,可以在以下时机执行:
- 方法执行前:在控制器方法执行前执行
- 方法执行后:在控制器方法执行后执行
- 异常抛出时:在异常抛出时执行
- 响应返回时:在响应返回时执行
📚 参考资源:
拦截器的核心特性 ⚡
- 基于 RxJS:使用 Observable 处理异步操作
- 功能强大:可以修改请求、响应、异常等
- 可组合:可以组合多个拦截器
- 易于测试:支持单元测试
拦截器的执行时机 📍
在 NestJS 的请求处理流程中,拦截器的位置如下:
请求 → 中间件 → 守卫 → 拦截器(前) → 管道 → 控制器 → 拦截器(后) → 响应拦截器在守卫之后、管道之前执行。
二、拦截器的工作原理
拦截器接口
拦截器需要实现 NestInterceptor 接口:
import {
Injectable,
NestInterceptor,
ExecutionContext,
CallHandler,
} from '@nestjs/common';
import { Observable } from 'rxjs';
@Injectable()
export class LoggingInterceptor implements NestInterceptor {
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
// 拦截逻辑
return next.handle(); // 调用下一个处理器
}
}核心概念解析
ExecutionContext
提供当前执行上下文的信息:
intercept(context: ExecutionContext, next: CallHandler) {
const request = context.switchToHttp().getRequest();
const response = context.switchToHttp().getResponse();
// 获取请求和响应对象
}CallHandler
提供调用下一个处理器的能力:
intercept(context: ExecutionContext, next: CallHandler) {
return next.handle(); // 继续执行后续逻辑
}Observable
拦截器返回 Observable,这是 RxJS 的核心概念。我们可以在 Observable 上使用操作符来修改数据流。
三、拦截器的执行时机
方法执行前
在方法执行前,可以记录日志、验证数据、修改请求:
intercept(context: ExecutionContext, next: CallHandler) {
const request = context.switchToHttp().getRequest();
console.log('请求前:', request.method, request.url);
return next.handle();
}方法执行后
在方法执行后,可以转换响应、记录日志、添加额外数据:
import { map } from 'rxjs/operators';
intercept(context: ExecutionContext, next: CallHandler) {
return next.handle().pipe(
map((data) => ({
code: 200,
message: '成功',
data: data,
})),
);
}异常处理
在异常抛出时,可以记录错误、转换异常、实现重试逻辑:
import { catchError } from 'rxjs/operators';
import { throwError } from 'rxjs';
intercept(context: ExecutionContext, next: CallHandler) {
return next.handle().pipe(
catchError((error) => {
console.error('拦截器捕获错误:', error);
return throwError(() => error);
}),
);
}四、实战:统一响应格式拦截器
需求分析 📋
创建一个统一的响应格式拦截器,将所有响应转换为标准格式:
{
"code": 200,
"message": "操作成功",
"data": { /* 实际数据 */ },
"timestamp": "2024-01-01T10:00:00.000Z",
"path": "/user"
}实现步骤
第一步:创建响应拦截器
// src/common/interceptors/response.interceptor.ts
import {
Injectable,
NestInterceptor,
ExecutionContext,
CallHandler,
HttpStatus,
} from '@nestjs/common';
import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';
export interface ResponseFormat<T = any> {
code: number;
message: string;
data: T;
timestamp: string;
path: string;
}
@Injectable()
export class ResponseInterceptor<T>
implements NestInterceptor<T, ResponseFormat<T>>
{
intercept(
context: ExecutionContext,
next: CallHandler,
): Observable<ResponseFormat<T>> {
const ctx = context.switchToHttp();
const request = ctx.getRequest();
return next.handle().pipe(
map((data) => {
// 处理空数据
if (data === null || data === undefined) {
return {
code: HttpStatus.OK,
message: '操作成功',
data: null,
timestamp: new Date().toISOString(),
path: request.url,
};
}
// 如果已经是标准格式,直接返回
if (
data &&
typeof data === 'object' &&
'code' in data &&
'message' in data
) {
return {
...data,
timestamp: new Date().toISOString(),
path: request.url,
};
}
// 标准成功响应格式
return {
code: HttpStatus.OK,
message: '操作成功',
data: data,
timestamp: new Date().toISOString(),
path: request.url,
};
}),
);
}
}第二步:注册全局拦截器
// src/app.module.ts
import { Module } from '@nestjs/common';
import { APP_INTERCEPTOR } from '@nestjs/core';
import { ResponseInterceptor } from './common/interceptors/response.interceptor';
@Module({
providers: [
{
provide: APP_INTERCEPTOR,
useClass: ResponseInterceptor,
},
],
})
export class AppModule {}第三步:测试验证 ✅
访问任意接口:
curl http://localhost:3000/user响应:
{
"code": 200,
"message": "操作成功",
"data": [
{ "id": 1, "name": "张三", "email": "zhangsan@example.com" }
],
"timestamp": "2024-01-01T10:00:00.000Z",
"path": "/user"
}五、常用 RxJS 操作符
map 操作符
用于转换数据:
import { map } from 'rxjs/operators';
return next.handle().pipe(
map((data) => ({ result: data })), // 转换数据结构
);tap 操作符
用于执行副作用,不修改数据:
import { tap } from 'rxjs/operators';
return next.handle().pipe(
tap((data) => {
console.log('响应数据:', data); // 记录日志
}),
);📚 参考资源:RxJS tap 操作符文档
catchError 操作符
用于捕获错误:
import { catchError } from 'rxjs/operators';
import { throwError } from 'rxjs';
return next.handle().pipe(
catchError((error) => {
console.error('错误:', error);
return throwError(() => error);
}),
);timeout 操作符
设置超时时间:
import { timeout } from 'rxjs/operators';
return next.handle().pipe(
timeout(5000), // 5秒超时
);retry 操作符
实现重试逻辑:
import { retry } from 'rxjs/operators';
return next.handle().pipe(
retry(3), // 失败后重试3次
);六、日志拦截器实战
创建日志拦截器
记录请求和响应的详细信息:
// src/common/interceptors/logging.interceptor.ts
import {
Injectable,
NestInterceptor,
ExecutionContext,
CallHandler,
Logger,
} from '@nestjs/common';
import { Observable } from 'rxjs';
import { tap } from 'rxjs/operators';
@Injectable()
export class LoggingInterceptor implements NestInterceptor {
private readonly logger = new Logger(LoggingInterceptor.name);
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
const request = context.switchToHttp().getRequest();
const { method, url } = request;
const now = Date.now();
return next.handle().pipe(
tap({
next: (data) => {
const response = context.switchToHttp().getResponse();
const { statusCode } = response;
const responseTime = Date.now() - now;
this.logger.log(
`${method} ${url} ${statusCode} - ${responseTime}ms`,
);
},
error: (error) => {
const responseTime = Date.now() - now;
this.logger.error(
`${method} ${url} - ${responseTime}ms - ${error.message}`,
);
},
}),
);
}
}注册日志拦截器
// src/app.module.ts
import { APP_INTERCEPTOR } from '@nestjs/core';
import { LoggingInterceptor } from './common/interceptors/logging.interceptor';
@Module({
providers: [
{
provide: APP_INTERCEPTOR,
useClass: LoggingInterceptor,
},
],
})
export class AppModule {}七、缓存拦截器实战
创建简单的缓存拦截器
// src/common/interceptors/cache.interceptor.ts
import {
Injectable,
NestInterceptor,
ExecutionContext,
CallHandler,
} from '@nestjs/common';
import { Observable, of } from 'rxjs';
import { tap } from 'rxjs/operators';
@Injectable()
export class CacheInterceptor implements NestInterceptor {
private cache = new Map<string, any>();
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
const request = context.switchToHttp().getRequest();
const { method, url } = request;
// 只缓存 GET 请求
if (method !== 'GET') {
return next.handle();
}
const cacheKey = url;
const cachedResponse = this.cache.get(cacheKey);
if (cachedResponse) {
return of(cachedResponse); // 返回缓存数据
}
return next.handle().pipe(
tap((data) => {
this.cache.set(cacheKey, data); // 缓存响应
}),
);
}
}⚠️ 注意:这是一个简单的内存缓存示例,实际项目中应该使用 Redis 等专业的缓存系统。
八、拦截器的应用范围
全局拦截器
在 app.module.ts 中注册:
@Module({
providers: [
{
provide: APP_INTERCEPTOR,
useClass: ResponseInterceptor,
},
],
})
export class AppModule {}控制器级别拦截器
使用 @UseInterceptors() 装饰器:
import { UseInterceptors } from '@nestjs/common';
import { LoggingInterceptor } from '../common/interceptors/logging.interceptor';
@Controller('user')
@UseInterceptors(LoggingInterceptor) // 应用到整个控制器
export class UserController {}方法级别拦截器
应用到特定方法:
@Controller('user')
export class UserController {
@Get()
@UseInterceptors(CacheInterceptor) // 仅应用到该方法
findAll() {
return [];
}
}多个拦截器组合
可以同时应用多个拦截器:
@UseInterceptors(LoggingInterceptor, CacheInterceptor, ResponseInterceptor)
export class UserController {}拦截器会按照声明顺序依次执行。
九、拦截器 vs 中间件
对比分析 📊
| 特性 | 中间件 | 拦截器 |
|---|---|---|
| 执行时机 | 路由匹配后 | 守卫之后、管道之前 |
| 依赖注入 | 类中间件支持 | 完全支持 |
| 修改响应 | 可以 | 可以,更灵活 |
| RxJS 支持 | 不支持 | 支持 |
| 异常处理 | 有限 | 强大 |
| 适用场景 | 请求预处理 | 响应转换、日志、缓存 |
选择建议 💡
- 中间件:适用于请求预处理,如 CORS、请求体解析、静态资源服务
- 拦截器:适用于响应转换、日志记录、缓存、性能监控
十、最佳实践
1. 使用全局拦截器处理通用逻辑 🌐
对于所有接口都需要的逻辑(如统一响应格式),使用全局拦截器:
// ✅ 推荐:全局统一响应格式
@Module({
providers: [
{ provide: APP_INTERCEPTOR, useClass: ResponseInterceptor },
],
})2. 使用局部拦截器处理特定逻辑 🎯
对于特定控制器或方法的逻辑,使用局部拦截器:
// ✅ 推荐:特定接口使用缓存
@Get()
@UseInterceptors(CacheInterceptor)
findAll() {}3. 保持拦截器简单 ✨
拦截器应该只做一件事,复杂的业务逻辑应该放在服务中:
// ✅ 推荐:拦截器只负责转换格式
intercept(context, next) {
return next.handle().pipe(
map((data) => ({ code: 200, data })),
);
}
// ❌ 避免:在拦截器中处理复杂业务逻辑
intercept(context, next) {
// 避免在这里进行数据库查询、复杂计算等
}4. 注意性能影响 ⚡
拦截器会在每个请求中执行,避免耗时操作:
// ✅ 推荐:轻量级操作
intercept(context, next) {
return next.handle().pipe(
tap(() => console.log('请求完成')),
);
}
// ❌ 避免:耗时操作
intercept(context, next) {
return next.handle().pipe(
tap(async () => {
await heavyOperation(); // 避免耗时操作
}),
);
}5. 合理使用缓存 💾
缓存可以提高性能,但要考虑缓存失效、内存占用等问题:
// ✅ 推荐:设置缓存过期时间
private cache = new Map<string, { data: any; expireAt: number }>();
intercept(context, next) {
const cached = this.cache.get(key);
if (cached && cached.expireAt > Date.now()) {
return of(cached.data);
}
// ...
}总结 🎯
这节课我们深入学习了 NestJS 拦截器的核心概念和实战应用。让我来总结一下要点:
核心知识点 ✅
- ✅ 理解了拦截器的定义:在方法执行前后添加额外逻辑的类
- ✅ 掌握了工作原理:基于 RxJS Observable 处理异步操作
- ✅ 理解了执行时机:在守卫之后、管道之前执行
- ✅ 掌握了 RxJS 操作符:
map、tap、catchError、timeout、retry - ✅ 完成了实战练习:统一响应格式、日志记录、缓存优化
- ✅ 理解了应用范围:全局、控制器级别、方法级别
- ✅ 掌握了最佳实践:保持简单、注意性能、合理使用缓存
拦截器应用场景 📊
| 场景 | 适用性 | 实现方式 |
|---|---|---|
| 统一响应格式 | ⭐⭐⭐⭐⭐ | 全局拦截器 + map 操作符 |
| 日志记录 | ⭐⭐⭐⭐⭐ | 全局拦截器 + tap 操作符 |
| 缓存优化 | ⭐⭐⭐⭐ | 局部拦截器 + of 操作符 |
| 性能监控 | ⭐⭐⭐⭐ | 全局拦截器 + 时间计算 |
| 异常转换 | ⭐⭐⭐ | 全局拦截器 + catchError 操作符 |
延伸学习资源 📚
- 本文链接:https://fridolph.top/posts/2025-10-27__nest10-interceptor
- 版权声明:本博客所有文章除特别声明外,均默认采用 CC BY-NC-SA 许可协议。