拦截器 Interceptor
本教程共 47 篇 · 第 18 篇 · 更新于 2026-08-09 · 约 13 分钟阅读
本节目标:搞懂拦截器的执行机制,学会用拦截器做响应转换、日志记录、异常处理、超时控制和缓存。
拦截器是什么
如果说守卫是小区的”门禁”,那拦截器更像是快递柜。
快递柜能在两个时间点介入:快递员存件的时候(请求进入控制器之前)和用户取件的时候(响应返回给客户端之前)。你可以在存件时做检查、在取件时做包装,整个过程就像”夹心饼干”一样把控制器方法包在中间。
用代码来说,拦截器是一个用 @Injectable() 装饰的类,它实现了 NestInterceptor 接口:
import { Injectable, NestInterceptor, ExecutionContext, CallHandler } from '@nestjs/common';
import { Observable } from 'rxjs';
@Injectable()
export class MyInterceptor implements NestInterceptor {
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
// 控制器方法执行前的逻辑
return next.handle().pipe(
// 控制器方法执行后的逻辑
);
}
}
两个参数:
ExecutionContext:和守卫里的一模一样,能拿到请求、控制器、方法等所有信息CallHandler:它的handle()方法返回一个Observable,代表控制器方法的执行流
Note如果你不调用
next.handle(),控制器方法就不会执行。这给了你完全的控制权——可以根据条件直接返回缓存数据,跳过控制器。
执行流程
拦截器横跨控制器方法的”前”和”后”两个阶段:
请求 → 守卫 → 拦截器(前) → 管道 → 控制器方法 → 拦截器(后) → 响应
intercept() 方法里,调用 next.handle() 之前的代码在控制器执行前运行,之后的代码在控制器执行后运行。
拦截器能干什么
官方文档列了五种典型用途:
- 方法执行前后添加额外逻辑(比如日志、计时)
- 转换返回值(比如统一响应格式)
- 转换异常(比如把底层异常映射成友好的错误信息)
- 扩展方法行为(比如给响应加额外字段)
- 完全覆盖方法(比如缓存命中时直接返回,不执行控制器)
这其实就是 AOP(面向切面编程)的思想——把通用逻辑从业务代码中抽出来,集中管理。
写一个日志拦截器
最经典的入门例子:记录每个请求的耗时。
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();
this.logger.log(`请求进入: ${method} ${url}`);
return next.handle().pipe(
tap(() => {
const duration = Date.now() - now;
this.logger.log(`请求完成: ${method} ${url} 耗时 ${duration}ms`);
}),
);
}
}
tap 是 RxJS 的操作符,它能在数据流经过时”偷看”一下,做一些副作用操作(比如打印日志),但不会改变数据本身。
Tip
tap特别适合做日志、监控这类”只看不改”的操作。数据经过tap之后原封不动地继续往下走。
响应转换拦截器
这个在实际项目中用得非常多。后端返回的数据格式经常需要统一包装,比如包一层 { code, message, data }。
如果每个控制器方法都手动包装,代码会很冗余。用拦截器就能一处定义、全局生效。
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;
timestamp: string;
}
@Injectable()
export class TransformInterceptor<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,
timestamp: new Date().toISOString(),
})),
);
}
}
控制器方法只需要返回原始数据:
@Get()
findAll() {
return [{ id: 1, name: '张三' }];
}
客户端收到的响应会自动变成:
{
"code": 200,
"message": "success",
"data": [{ "id": 1, "name": "张三" }],
"timestamp": "2026-08-09T00:00:00.000Z"
}
Warning响应转换拦截器不能和
@Res()一起用。一旦你用了@Res()直接操作响应对象,NestJS 就失去了对响应的控制权,拦截器的map操作会失效。
异常映射拦截器
有时候底层抛出的异常不够友好,你想统一转换一下。拦截器里的 catchError 操作符就是干这个的。
import { Injectable, NestInterceptor, ExecutionContext, CallHandler, NotFoundException } from '@nestjs/common';
import { Observable, throwError } from 'rxjs';
import { catchError } from 'rxjs/operators';
@Injectable()
export class ErrorInterceptor implements NestInterceptor {
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
return next.handle().pipe(
catchError(error => {
// 把数据库的"记录不存在"错误转换成 404
if (error.code === 'P2025') {
return throwError(() => new NotFoundException('记录不存在'));
}
// 其他错误原样抛出
return throwError(() => error);
}),
);
}
}
Note拦截器里抛出的异常和控制器里抛出的异常一样,会被 NestJS 的异常过滤器统一处理。你不用担心异常”漏出去”。
超时拦截器
接口响应太慢?加个超时保护。
import { Injectable, NestInterceptor, ExecutionContext, CallHandler, RequestTimeoutException } from '@nestjs/common';
import { Observable, throwError, TimeoutError } from 'rxjs';
import { timeout, catchError } from 'rxjs/operators';
@Injectable()
export class TimeoutInterceptor implements NestInterceptor {
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
return next.handle().pipe(
timeout(5000), // 5 秒超时
catchError(err => {
if (err instanceof TimeoutError) {
return throwError(() => new RequestTimeoutException('请求超时,请稍后重试'));
}
return throwError(() => err);
}),
);
}
}
timeout(5000) 是 RxJS 的操作符,如果 5 秒内数据流没有返回数据,就会抛出一个 TimeoutError。拦截器捕获它,转换成 RequestTimeoutException(HTTP 408)。
缓存拦截器
拦截器还有一个”大招”——完全跳过控制器方法,直接返回数据。
import { Injectable, NestInterceptor, ExecutionContext, CallHandler } from '@nestjs/common';
import { Observable, of } from 'rxjs';
import { tap } from 'rxjs/operators';
@Injectable()
export class SimpleCacheInterceptor implements NestInterceptor {
private cache = new Map<string, any>();
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
const request = context.switchToHttp().getRequest();
const key = `${request.method}:${request.url}`;
// 缓存命中 → 直接返回,控制器不会执行
if (this.cache.has(key)) {
return of(this.cache.get(key));
}
// 缓存未命中 → 正常执行控制器,并缓存结果
return next.handle().pipe(
tap(data => {
this.cache.set(key, data);
}),
);
}
}
关键点在于 of() 这个 RxJS 操作符。它把一个普通值包装成 Observable 返回。当你返回 of(cachedData) 时,next.handle() 根本没被调用,控制器方法自然就不会执行。
Tip这个例子只是演示原理。实际项目中建议用
@nestjs/cache-manager提供的缓存方案,它支持 TTL、缓存失效、多种存储后端等高级特性。
拦截器的三种绑定方式
和守卫完全一致,支持方法级、控制器级、全局级。
方法级别
@Controller('users')
export class UsersController {
@Get()
@UseInterceptors(LoggingInterceptor)
findAll() {
return [];
}
}
控制器级别
@Controller('users')
@UseInterceptors(LoggingInterceptor)
export class UsersController {
// 所有方法都会被 LoggingInterceptor 拦截
}
全局级别
// 方式一:在 main.ts 中注册(无法注入依赖)
const app = await NestFactory.create(AppModule);
app.useGlobalInterceptors(new LoggingInterceptor());
// 方式二:在模块中注册(推荐,支持依赖注入)
import { Module } from '@nestjs/common';
import { APP_INTERCEPTOR } from '@nestjs/core';
@Module({
providers: [
{
provide: APP_INTERCEPTOR,
useClass: LoggingInterceptor,
},
],
})
export class AppModule {}
Tip和守卫一样,全局拦截器用
APP_INTERCEPTOR注册才能注入依赖。
多个拦截器的执行顺序
你可以同时挂多个拦截器:
@UseInterceptors(LoggingInterceptor, TransformInterceptor)
执行顺序像”洋葱模型”——先进后出:
LoggingInterceptor(前) → TransformInterceptor(前) →
控制器方法
→ TransformInterceptor(后) → LoggingInterceptor(后)
第一个拦截器的”前”逻辑最先执行,它的”后”逻辑最后执行。
Note这个顺序很重要。比如日志拦截器应该放在最外层(数组第一个),这样它才能记录包括其他拦截器在内的完整耗时。
性能监控拦截器
把日志拦截器稍微改一下,就能做一个慢请求监控:
@Injectable()
export class PerformanceInterceptor implements NestInterceptor {
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
const startTime = Date.now();
const request = context.switchToHttp().getRequest();
return next.handle().pipe(
tap(() => {
const duration = Date.now() - startTime;
const { method, url } = request;
if (duration > 1000) {
console.warn(`慢请求警告: ${method} ${url} 耗时 ${duration}ms`);
}
}),
);
}
}
超过 1 秒的请求会被标记出来。生产环境里可以把阈值调低一些,或者把告警接入监控系统。
拦截器最佳实践
1. 单一职责
每个拦截器只做一件事。日志归日志,转换归转换,缓存归缓存。
// 好的做法
@UseInterceptors(LoggingInterceptor, TransformInterceptor, CacheInterceptor)
// 不好的做法:一个拦截器干所有事
@UseInterceptors(DoEverythingInterceptor)
2. 用装饰器传递配置
不要在拦截器里硬编码,用自定义装饰器把配置贴在路由上:
import { SetMetadata } from '@nestjs/common';
export const CACHE_TTL_KEY = 'cache_ttl';
export const CacheTTL = (seconds: number) => SetMetadata(CACHE_TTL_KEY, seconds);
@Get()
@CacheTTL(120) // 缓存 120 秒
findAll() {}
3. 善用 RxJS 操作符
拦截器的威力来自 RxJS。常用的几个操作符:
tap:做副作用,不改数据(日志、计时)map:转换数据(响应格式化)catchError:捕获异常(异常映射)timeout:超时控制filter:过滤数据
4. 拦截器和 @Res() 不兼容
用了 @Res() 就拿不到标准的响应流了,拦截器的 map 等操作符会失效。如果某个方法需要 @Res(),就别给它挂响应转换拦截器。
小结
拦截器的核心价值是”在控制器方法的前后插入通用逻辑”。
关键知识点回顾:
- 拦截器实现
NestInterceptor接口,核心方法是intercept() next.handle()返回 Observable,代表控制器的执行流- 不调用
next.handle()可以跳过控制器(缓存场景) - 用
map转换响应、catchError转换异常、tap做副作用 - 三种绑定方式:方法级、控制器级、全局级
- 全局拦截器用
APP_INTERCEPTOR注册才能注入依赖 - 多个拦截器按”洋葱模型”执行,先进后出
下一章我们学自定义装饰器,让代码写起来更简洁。