NestJS 的 拦截器(Interceptor) 是一个强大的概念,它基于 面向切面编程(AOP) 思想,让你能够在方法执行前后插入自定义逻辑。
拦截器能做什么?
拦截器的功能非常丰富,常见的应用场景包括:
- 在方法执行前后执行额外逻辑
- 转换/映射函数的返回结果
- 转换/覆盖函数抛出的异常
- 扩展函数的基本行为
- 完全覆盖函数的行为(比如缓存)
拦截器基础
每个拦截器都需要实现 NestInterceptor 接口,它要求实现 intercept 方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| import { Injectable, NestInterceptor, ExecutionContext, CallHandler } from '@nestjs/common'; import { Observable } from 'rxjs'; import { tap } from 'rxjs/operators';
@Injectable() export class LoggingInterceptor implements NestInterceptor { intercept(context: ExecutionContext, next: CallHandler): Observable<any> { console.log('进入拦截器...'); const now = Date.now(); return next .handle() .pipe( tap(() => console.log(`请求耗时: ${Date.now() - now}ms`)), ); } }
|
关键点:
ExecutionContext 与守卫中相同,提供当前请求的上下文
CallHandler 的 handle() 方法返回一个 Observable,调用它才会执行路由处理器
- 不调用
handle(),路由处理器就不会执行
使用拦截器
绑定到单个路由
1 2 3 4 5 6 7 8 9 10 11
| import { Controller, Get, UseInterceptors } from '@nestjs/common'; import { LoggingInterceptor } from './logging.interceptor';
@Controller('users') export class UsersController { @Get('profile') @UseInterceptors(LoggingInterceptor) getProfile() { return { name: 'Alice', age: 30 }; } }
|
绑定到整个控制器
1 2 3 4 5 6 7 8
| @Controller('users') @UseInterceptors(LoggingInterceptor) export class UsersController { @Get('profile') getProfile() { return 'profile'; } }
|
全局拦截器
1 2 3 4 5 6 7 8 9 10 11
| import { NestFactory } from '@nestjs/core'; import { AppModule } from './app.module'; import { LoggingInterceptor } from './logging.interceptor';
async function bootstrap() { const app = await NestFactory.create(AppModule); app.useGlobalInterceptors(new LoggingInterceptor()); await app.listen(3000); } bootstrap();
|
通过模块注册全局拦截器(支持依赖注入):
1 2 3 4 5 6 7 8 9 10 11 12 13
| import { Module } from '@nestjs/common'; import { APP_INTERCEPTOR } from '@nestjs/core'; import { LoggingInterceptor } from './logging.interceptor';
@Module({ providers: [ { provide: APP_INTERCEPTOR, useClass: LoggingInterceptor, }, ], }) export class AppModule {}
|
RxJS 操作符在拦截器中的应用
拦截器的核心在于 RxJS 的能力。以下是常用的操作符场景:
tap — 不修改响应,只做副作用
1 2 3 4 5 6 7 8 9 10 11
| import { tap } from 'rxjs/operators';
intercept(context: ExecutionContext, next: CallHandler): Observable<any> { return next.handle().pipe( tap({ next: (data) => console.log('响应数据:', data), error: (err) => console.error('发生错误:', err), complete: () => console.log('请求完成'), }), ); }
|
map — 转换响应数据
1 2 3 4 5 6 7 8 9 10 11
| import { map } from 'rxjs/operators';
intercept(context: ExecutionContext, next: CallHandler): Observable<any> { return next.handle().pipe( map((data) => ({ code: 200, message: 'success', data, })), ); }
|
catchError — 捕获并处理异常
1 2 3 4 5 6 7 8 9 10 11
| import { catchError } from 'rxjs/operators'; import { throwError } from 'rxjs';
intercept(context: ExecutionContext, next: CallHandler): Observable<any> { return next.handle().pipe( catchError((err) => { console.error('拦截到错误:', err.message); return throwError(() => err); }), ); }
|
timeout — 超时控制
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| import { timeout } from 'rxjs/operators'; import { RequestTimeoutException } from '@nestjs/common';
intercept(context: ExecutionContext, next: CallHandler): Observable<any> { return next.handle().pipe( timeout(5000), catchError((err) => { if (err.name === 'TimeoutError') { return throwError(() => new RequestTimeoutException('请求超时')); } return throwError(() => err); }), ); }
|
实战案例
1. 统一响应格式
将所有接口的返回值包装成统一的 { code, message, data } 格式:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
| import { Injectable, NestInterceptor, ExecutionContext, CallHandler } from '@nestjs/common'; import { Observable } from 'rxjs'; import { map } from 'rxjs/operators';
export interface ApiResponse<T> { code: number; message: string; data: T; }
@Injectable() export class ResponseInterceptor<T> implements NestInterceptor<T, ApiResponse<T>> { intercept( context: ExecutionContext, next: CallHandler, ): Observable<ApiResponse<T>> { return next.handle().pipe( map((data) => ({ code: 200, message: 'success', data, })), ); } }
|
全局注册后,控制器只需返回原始数据:
1 2 3 4 5 6 7 8
| @Controller('users') export class UsersController { @Get('profile') getProfile() { return { name: 'Alice', age: 30 }; } }
|
2. 请求日志拦截器
记录每个请求的详细信息,包括参数、耗时、响应状态:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30
| import { Injectable, NestInterceptor, ExecutionContext, CallHandler, Logger } from '@nestjs/common'; import { Observable } from 'rxjs'; import { tap } from 'rxjs/operators';
@Injectable() export class RequestLogInterceptor implements NestInterceptor { private readonly logger = new Logger('HTTP');
intercept(context: ExecutionContext, next: CallHandler): Observable<any> { const request = context.switchToHttp().getRequest(); const { method, url, body, query } = request; const now = Date.now();
return next.handle().pipe( tap({ next: (data) => { this.logger.log( `${method} ${url} ${Date.now() - now}ms - 参数: ${JSON.stringify({ query, body })}`, ); }, error: (error) => { this.logger.error( `${method} ${url} ${Date.now() - now}ms - 错误: ${error.message}`, ); }, }), ); } }
|
3. 缓存拦截器
对 GET 请求的结果进行简单的内存缓存:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33
| 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 readonly cache = new Map<string, any>();
intercept(context: ExecutionContext, next: CallHandler): Observable<any> { const request = context.switchToHttp().getRequest();
if (request.method !== 'GET') { return next.handle(); }
const cacheKey = request.url; const cachedResponse = this.cache.get(cacheKey);
if (cachedResponse) { return of(cachedResponse); }
return next.handle().pipe( tap((data) => { this.cache.set(cacheKey, data); setTimeout(() => this.cache.delete(cacheKey), 60000); }), ); } }
|
4. 敏感字段过滤拦截器
自动移除响应中的敏感字段(如密码、token):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30
| import { Injectable, NestInterceptor, ExecutionContext, CallHandler } from '@nestjs/common'; import { Observable } from 'rxjs'; import { map } from 'rxjs/operators';
@Injectable() export class SanitizeInterceptor implements NestInterceptor { intercept(context: ExecutionContext, next: CallHandler): Observable<any> { return next.handle().pipe( map((data) => this.removeSensitiveFields(data)), ); }
private removeSensitiveFields(data: any): any { if (Array.isArray(data)) { return data.map((item) => this.removeSensitiveFields(item)); }
if (data && typeof data === 'object') { const sensitiveFields = ['password', 'token', 'secret', 'refreshToken']; const cleaned = { ...data }; for (const field of sensitiveFields) { delete cleaned[field]; } return cleaned; }
return data; } }
|
多个拦截器的执行顺序
多个拦截器的执行顺序遵循栈的原则:
1 2 3 4 5 6 7 8 9
| @Controller('users') @UseInterceptors(InterceptorA) export class UsersController { @Get('profile') @UseInterceptors(InterceptorB) getProfile() { return 'profile'; } }
|
执行顺序:
InterceptorA.intercept() 的前置代码
InterceptorB.intercept() 的前置代码
- 路由处理器
getProfile()
InterceptorB.intercept() 的后置代码
InterceptorA.intercept() 的后置代码
拦截器 vs 守卫 vs 管道 vs 中间件
| 组件 |
执行时机 |
主要用途 |
| 中间件 |
最先执行 |
日志、CORS、请求体解析 |
| 守卫 |
中间件之后 |
鉴权、权限校验 |
| 管道 |
守卫之后 |
参数校验、类型转换 |
| 拦截器 |
管道之后 |
响应映射、缓存、日志、异常处理 |
总结
NestJS 拦截器是最灵活的组件之一,它利用 RxJS 的强大能力让你能精细控制请求-响应周期:
- AOP 思想:在方法执行前后插入逻辑,实现关注点分离
- RxJS 驱动:借助
tap、map、catchError、timeout 等操作符处理异步流
- 完全控制:可以选择不执行路由处理器(如缓存场景)
- 关注点分离:将统一响应格式、日志记录、缓存等横切关注点集中管理
结合之前介绍的中间件、守卫,加上拦截器,你就掌握了 NestJS 请求处理链路上的所有关卡,可以根据不同需求选择最合适的组件。