【NestJS】10 拦截器(Interceptor)完全指南

2524 字
13 分钟
【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"
}

实现步骤#

第一步:创建响应拦截器

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 {}

第三步:测试验证

访问任意接口:

Terminal window
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 操作符maptapcatchErrortimeoutretry
  • 完成了实战练习:统一响应格式、日志记录、缓存优化
  • 理解了应用范围:全局、控制器级别、方法级别
  • 掌握了最佳实践:保持简单、注意性能、合理使用缓存

拦截器应用场景 📊#

场景适用性实现方式
统一响应格式⭐⭐⭐⭐⭐全局拦截器 + map 操作符
日志记录⭐⭐⭐⭐⭐全局拦截器 + tap 操作符
缓存优化⭐⭐⭐⭐局部拦截器 + of 操作符
性能监控⭐⭐⭐⭐全局拦截器 + 时间计算
异常转换⭐⭐⭐全局拦截器 + catchError 操作符

延伸学习资源 📚#

支持与分享

如果这篇文章对你有帮助,欢迎分享给更多人或赞助支持!

【NestJS】10 拦截器(Interceptor)完全指南
https://blog.fridolph.top/posts/2025-10-27__nest10-interceptor/
作者
Fridolph
发布于
2025-10-27
许可协议
CC BY-NC-SA 4.0

评论区

Profile Image of the Author
Fridolph
热爱 Coding、音乐和羽毛球的 90 后全栈工程师
公告
欢迎访问我的小站 ^_^ 我是昇哥,热爱Coding,喜爱音乐、羽毛球和摄影的 90后全栈工程师
分类
标签

文章目录