【NestJS】10 拦截器(Interceptor)完全指南
这一篇,我们将深入学习 拦截器(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"}实现步骤
第一步:创建响应拦截器
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, }; }), ); }}第二步:注册全局拦截器
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次);六、日志拦截器实战
创建日志拦截器
记录请求和响应的详细信息:
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}`, ); }, }), ); }}注册日志拦截器
import { APP_INTERCEPTOR } from '@nestjs/core';import { LoggingInterceptor } from './common/interceptors/logging.interceptor';
@Module({ providers: [ { provide: APP_INTERCEPTOR, useClass: LoggingInterceptor, }, ],})export class AppModule {}七、缓存拦截器实战
创建简单的缓存拦截器
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 操作符 |
延伸学习资源 📚
支持与分享
如果这篇文章对你有帮助,欢迎分享给更多人或赞助支持!