Skip to content
第 10 章 后端 ⏱ 13 分钟阅读

第 10 章:拦截器 ​

学习目标 ​

  • 理解 Interceptor 的作用时机
  • 写日志、缓存、转换拦截器
  • 掌握 rxjs 操作符
  • 避开 3 个拦截器坑

一、Interceptor 能干什么 ​

在 Controller 前后插入逻辑,基于 AOP 思想:

  • 改返回值
  • 改异常
  • 计时/打日志
  • 缓存响应

执行顺序:Guard → Pipe → Controller → Interceptor(after) → 响应。

二、最简单 Interceptor ​

typescript
// 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`)),
    );
  }
}

挂载:

typescript
@UseInterceptors(LoggingInterceptor)
@Controller('users')
export class UsersController {}

RxJS 基础(Interceptor 必备):

Interceptor 操作 next.handle() 返回的 Observable,常用 4 个操作符:

操作符作用类比
map改数据 —— 转换值数组的 .map()
tap不改数据 —— 偷看值,做副作用(日志/埋点)数组的 .forEach()(只看不动)
pipe串起多个操作符数组的链式调用
subscribe触发流 —— Observable 不订阅就不执行"开水龙头"

map vs 数组 map:

typescript
// 数组:同步,立刻返回
[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, 6

tap = 只看不改:

typescript
of(1, 2, 3).pipe(
  tap(x => console.log('流过:', x)), // 不改数据
  map(x => x * 2),                    // 改数据
).subscribe(console.log);
// 流过: 1
// 流过: 2
// 流过: 3
// 2, 4, 6

subscribe = 触发流:

typescript
of(1, 2, 3).pipe(map(x => x * 2));
// ↑ 啥也没发生,只有"流定义"

of(1, 2, 3).pipe(map(x => x * 2)).subscribe(console.log);
// ↑ 订阅后才输出:2, 4, 6

subscribe 的 3 个回调:

typescript
observable.subscribe({
  next: v => {},        // 正常值
  error: e => {},       // 出错
  complete: () => {},   // 流结束
});

pipe() 必须用,不能直接链:

现代 RxJS(6+)操作符都是独立函数,必须通过 pipe() 串起来:

typescript
// ❌ 错误(老 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 数组:

typescript
// 数组:有内置方法,直接链
[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 套路:

typescript
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 帮你订阅)

三、改返回值(包装) ​

typescript
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: ... }。

四、统一异常处理 ​

typescript
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。

五、缓存拦截器 ​

typescript
@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 章)。

六、超时拦截器 ​

typescript
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);
      }),
    );
  }
}

七、全局拦截器 ​

typescript
// main.ts
app.useGlobalInterceptors(new LoggingInterceptor(), new TransformInterceptor());

或 DI:

typescript
{ provide: APP_INTERCEPTOR, useClass: TransformInterceptor }

八、Interceptor vs Pipe vs Guard ​

GuardPipeInterceptor
何时之前参数处理Controller 前后
能改 req否是(单个参数)否
能改 res否否是(返回值)
异步可可必须返回 Observable

九、实战:响应包装 + 计时 ​

typescript
@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`,
      })),
    );
  }
}

测试:

bash
curl http://localhost:3000/users
# {"code":0,"message":"success","data":[...],"duration":"3ms"}

⚠️ 坑 2:Interceptor 里 next.handle() 不调 → 请求卡死,响应永远不出。

十、组合多个拦截器 ​

typescript
@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。

typescript
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

多拦截器场景示例:

typescript
@UseInterceptors(LoggingInterceptor, TransformInterceptor)
class XxxController {}

// LoggingInterceptor 里的 tap,看到的不是 Controller 返回的原始 data
// 而是 [已经被 TransformInterceptor 包过] 的 data

记忆口诀:

  • tap 在 map 前 = 看原 data
  • tap 在 map 后 = 看改后 data
  • 多拦截器 = 外层 tap 看到的永远是"内层已经加工完"的 data
  • 洋葱模型 = 从外到内进,从内到外收尾

十一、本章小结 ​

要点关键
时机Controller 前后
必返回Observable(基于 rxjs)
包装返回值map(data => ...)
缓存tap + Map/Redis
超时timeout(ms) 操作符
改异常catchError(慎用)

动手练习 ​

  1. 打日志:写 LoggingInterceptor,打印 method、URL、耗时
  2. 包装:写 TransformInterceptor,统一 { code, data, ts }
  3. 缓存:对 GET /config 加 60 秒内存缓存

下一章:第 11 章:自定义装饰器 →

本站基于 VitePress 构建 · 由 StackHub 团队维护