第 10 章:拦截器
学习目标
- 理解 Interceptor 的作用时机
- 写日志、缓存、转换拦截器
- 掌握 rxjs 操作符
- 避开 3 个拦截器坑
一、Interceptor 能干什么
在 Controller 前后插入逻辑,基于 AOP 思想:
- 改返回值
- 改异常
- 计时/打日志
- 缓存响应
执行顺序:Guard → Pipe → Controller → Interceptor(after) → 响应。
二、最简单 Interceptor
// interceptors/logging.interceptor.ts
import { CallHandler, ExecutionContext, Injectable, NestInterceptor } from '@nestjs/common';
import { Observable, tap } from 'rxjs';
@Injectable()
export class LoggingInterceptor implements NestInterceptor {
intercept(ctx: ExecutionContext, next: CallHandler): Observable<any> {
const start = Date.now();
console.log(`Before ${ctx.getClass().name}.${ctx.getHandler().name}`);
return next.handle().pipe(
tap(() => console.log(`After ${Date.now() - start}ms`)),
);
}
}挂载:
@UseInterceptors(LoggingInterceptor)
@Controller('users')
export class UsersController {}RxJS 基础(Interceptor 必备):
Interceptor 操作 next.handle() 返回的 Observable,常用 4 个操作符:
| 操作符 | 作用 | 类比 |
|---|---|---|
map | 改数据 —— 转换值 | 数组的 .map() |
tap | 不改数据 —— 偷看值,做副作用(日志/埋点) | 数组的 .forEach()(只看不动) |
pipe | 串起多个操作符 | 数组的链式调用 |
subscribe | 触发流 —— Observable 不订阅就不执行 | "开水龙头" |
map vs 数组 map:
// 数组:同步,立刻返回
[1, 2, 3].map(x => x * 2); // [2, 4, 6]
// Observable:异步,流式输出
of(1, 2, 3).pipe(map(x => x * 2)).subscribe(console.log);
// 输出:2, 4, 6tap = 只看不改:
of(1, 2, 3).pipe(
tap(x => console.log('流过:', x)), // 不改数据
map(x => x * 2), // 改数据
).subscribe(console.log);
// 流过: 1
// 流过: 2
// 流过: 3
// 2, 4, 6subscribe = 触发流:
of(1, 2, 3).pipe(map(x => x * 2));
// ↑ 啥也没发生,只有"流定义"
of(1, 2, 3).pipe(map(x => x * 2)).subscribe(console.log);
// ↑ 订阅后才输出:2, 4, 6subscribe 的 3 个回调:
observable.subscribe({
next: v => {}, // 正常值
error: e => {}, // 出错
complete: () => {}, // 流结束
});pipe() 必须用,不能直接链:
现代 RxJS(6+)操作符都是独立函数,必须通过 pipe() 串起来:
// ❌ 错误(老 RxJS 5 写法,已废弃)
of(1, 2, 3).tap(x => console.log(x)).map(x => x * 2);
// TypeError: of(...).tap is not a function
// ✅ 正确(RxJS 6+ 唯一写法)
of(1, 2, 3).pipe(
tap(x => console.log(x)),
map(x => x * 2),
);| 写法 | 版本 | 状态 |
|---|---|---|
obs.map(fn).filter(fn) | RxJS 5 及更早 | ⚠️ 已废弃 |
obs.pipe(map(fn), filter(fn)) | RxJS 6+ | ✅ 唯一正确写法 |
Observable vs 数组:
// 数组:有内置方法,直接链
[1, 2, 3].map(x => x * 2).filter(x => x > 2);
// Observable:只有 subscribe,操作符要 pipe 进来
of(1, 2, 3).pipe(map(x => x * 2), filter(x => x > 2));pipe() 的好处:Tree-shakable(没用到的操作符不打包)、纯函数易测试、不污染 Observable 原型。
Interceptor 里谁订阅了? —— NestJS 框架内部订阅,自动 res.json() 发响应:
Controller return data
↓
next.handle() → Observable<data>
↓
Interceptor 的 pipe(tap, map, ...) ← 你只加工
↓
NestJS 内部 subscribe → 拿到最终值 → res.json() → 发给客户端所以 Interceptor 里你只 pipe,不 subscribe。
完整 Interceptor 套路:
return next.handle().pipe(
tap(data => console.log('看 data:', data)), // ← 1. 偷看(不改)
map(data => ({ code: 0, data })), // ← 2. 改数据(包装响应)
);记忆口诀:
pipe()= 装过滤器(改/看数据)subscribe()= 开水龙头(数据才开始流)map= 改数据tap= 看数据(不改)- Interceptor = 只
pipe,不subscribe(NestJS 帮你订阅)
三、改返回值(包装)
import { map } from 'rxjs/operators';
@Injectable()
export class TransformInterceptor implements NestInterceptor {
intercept(_ctx: ExecutionContext, next: CallHandler): Observable<any> {
return next.handle().pipe(
map(data => ({ code: 0, msg: 'ok', data, ts: Date.now() })),
);
}
}Controller 返回啥都会被包成 { code: 0, msg: 'ok', data: ..., ts: ... }。
四、统一异常处理
import { catchError, throwError } from 'rxjs';
@Injectable()
export class ErrorsInterceptor implements NestInterceptor {
intercept(_ctx, next) {
return next.handle().pipe(
catchError(err => throwError(() => new InternalServerErrorException(err.message))),
);
}
}⚠️ 坑 1:在 Interceptor 里把
NotFoundException转成 500 → 状态码丢了,推荐用全局 ExceptionFilter。
五、缓存拦截器
@Injectable()
export class CacheInterceptor implements NestInterceptor {
private cache = new Map<string, any>();
intercept(ctx: ExecutionContext, next: CallHandler): Observable<any> {
const key = ctx.switchToHttp().getRequest().url;
if (this.cache.has(key)) return of(this.cache.get(key));
return next.handle().pipe(
tap(data => this.cache.set(key, data)),
);
}
}简单场景够用,生产建议用 Redis(见第 23 章)。
六、超时拦截器
import { timeout } from 'rxjs/operators';
@Injectable()
export class TimeoutInterceptor implements NestInterceptor {
intercept(_ctx, next: CallHandler): Observable<any> {
return next.handle().pipe(
// 1️⃣ 5 秒内 Controller 没返回值 → 自动抛 TimeoutError(RxJS 内部错误)
timeout(5000),
// 2️⃣ 捕获上游错误,把 RxJS 的 TimeoutError 翻译成 NestJS 的 HTTP 408 异常
catchError(err => {
if (err.name === 'TimeoutError') {
// 超时 → 包成 RequestTimeoutException(HTTP 408)
return throwError(() => new RequestTimeoutException('timeout'));
}
// 其他错误 → 原样抛出去,不走超时分支
return throwError(() => err);
}),
);
}
}七、全局拦截器
// main.ts
app.useGlobalInterceptors(new LoggingInterceptor(), new TransformInterceptor());或 DI:
{ provide: APP_INTERCEPTOR, useClass: TransformInterceptor }八、Interceptor vs Pipe vs Guard
| Guard | Pipe | Interceptor | |
|---|---|---|---|
| 何时 | 之前 | 参数处理 | Controller 前后 |
| 能改 req | 否 | 是(单个参数) | 否 |
| 能改 res | 否 | 否 | 是(返回值) |
| 异步 | 可 | 可 | 必须返回 Observable |
九、实战:响应包装 + 计时
@Injectable()
export class WrapperInterceptor implements NestInterceptor {
intercept(ctx: ExecutionContext, next: CallHandler) {
const start = Date.now();
return next.handle().pipe(
map(data => ({
code: 0,
message: 'success',
data,
duration: `${Date.now() - start}ms`,
})),
);
}
}测试:
curl http://localhost:3000/users
# {"code":0,"message":"success","data":[...],"duration":"3ms"}⚠️ 坑 2:Interceptor 里
next.handle()不调 → 请求卡死,响应永远不出。
十、组合多个拦截器
@UseInterceptors(LoggingInterceptor, TransformInterceptor, TimeoutInterceptor)
@Controller('orders')
export class OrdersController {}执行顺序:Logging → Transform → Timeout → Controller → Timeout 收尾 → Transform 包装 → Logging 收尾。
⚠️ 坑 3:
map()改了 data,后面tap()看到的是 [改后] 的 data,不是原始 data。
坑 3 详解:
管道里的 tap 看到的是"到它这一步"为止的数据,不是 Controller 最初返回的原始 data。
next.handle().pipe(
tap(data => console.log('原:', data)), // 看到的是"到这一步为止"的值
map(data => ({ ...data, wrapped: true })), // ← data 被改
tap(data => console.log('改后:', data)), // 看到的是 [被 map 改过] 的 data!
);3 条链路顺序规则:
tap 位置 | 看到的 data |
|---|---|
tap 在 map 之前 | 原始 data |
tap 在 map 之后 | [改后] data |
| 多个拦截器 | 外层 tap 看到的是内层所有 map 加工完的 data |
多拦截器场景示例:
@UseInterceptors(LoggingInterceptor, TransformInterceptor)
class XxxController {}
// LoggingInterceptor 里的 tap,看到的不是 Controller 返回的原始 data
// 而是 [已经被 TransformInterceptor 包过] 的 data记忆口诀:
tap在map前 = 看原 datatap在map后 = 看改后 data- 多拦截器 = 外层 tap 看到的永远是"内层已经加工完"的 data
- 洋葱模型 = 从外到内进,从内到外收尾
十一、本章小结
| 要点 | 关键 |
|---|---|
| 时机 | Controller 前后 |
| 必返回 | Observable(基于 rxjs) |
| 包装返回值 | map(data => ...) |
| 缓存 | tap + Map/Redis |
| 超时 | timeout(ms) 操作符 |
| 改异常 | catchError(慎用) |
动手练习
- 打日志:写
LoggingInterceptor,打印 method、URL、耗时 - 包装:写
TransformInterceptor,统一{ code, data, ts } - 缓存:对
GET /config加 60 秒内存缓存
下一章:第 11 章:自定义装饰器 →