第 175 章:拦截器(Interceptor)
学习目标
- 理解 Interceptor 的作用
- 掌握 AOP 风格编程
- 实现日志、缓存、转换
- 学会异常映射
一、什么是 Interceptor
Interceptor 用于拦截方法调用前后,可用于:
- 在方法执行前添加逻辑
- 在方法执行后转换结果
- 捕获异常并转换
- 缓存响应
二、基础结构
typescript
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('Before...');
const now = Date.now();
return next.handle().pipe(
tap(() => console.log(`After... ${Date.now() - now}ms`)),
);
}
}三、常见场景
3.1 响应转换(最常用)
typescript
import { map } from 'rxjs/operators';
@Injectable()
export class TransformInterceptor implements NestInterceptor {
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
return next.handle().pipe(
map(data => ({
code: 200,
message: 'OK',
data,
timestamp: Date.now(),
})),
);
}
}
// 全局注册
app.useGlobalInterceptors(new TransformInterceptor());
// 输出
// { code: 200, message: 'OK', data: { id: 1, name: 'Tom' }, timestamp: 1690000000000 }3.2 日志拦截
typescript
@Injectable()
export class LoggingInterceptor implements NestInterceptor {
private readonly logger = new Logger(LoggingInterceptor.name);
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
const req = context.switchToHttp().getRequest();
const { method, url, body, query, params, headers } = req;
const userAgent = headers['user-agent'] || '';
const ip = req.ip;
this.logger.log(`→ ${method} ${url} ${ip} ${userAgent}`);
const now = Date.now();
return next.handle().pipe(
tap({
next: (data) => {
this.logger.log(`← ${method} ${url} ${Date.now() - now}ms`);
},
error: (err) => {
this.logger.error(`✗ ${method} ${url} ${Date.now() - now}ms ${err.message}`);
},
}),
);
}
}3.3 超时拦截
typescript
import { timeout, catchError } from 'rxjs/operators';
import { throwError } from 'rxjs';
@Injectable()
export class TimeoutInterceptor implements NestInterceptor {
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);
}),
);
}
}3.4 异常映射
typescript
import { catchError, throwError } from 'rxjs';
@Injectable()
export class ErrorsInterceptor implements NestInterceptor {
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
return next.handle().pipe(
catchError(err => throwError(() => new BadRequestException(err.message))),
);
}
}四、RxJS 操作符
typescript
import { Observable, of } from 'rxjs';
import { map, tap, mergeMap, catchError } from 'rxjs/operators';
@Injectable()
export class CacheInterceptor implements NestInterceptor {
private cache = new Map<string, any>();
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
const req = context.switchToHttp().getRequest();
const key = `${req.method}:${req.url}`;
if (this.cache.has(key)) {
// 命中缓存,直接返回
return of(this.cache.get(key));
}
return next.handle().pipe(
tap(data => this.cache.set(key, data)), // 缓存结果
);
}
}五、参数处理
5.1 在 Interceptor 中获取 Controller/Handler
typescript
intercept(context: ExecutionContext, next: CallHandler) {
const controller = context.getClass().name; // 类名
const handler = context.getHandler().name; // 方法名
console.log(`${controller}.${handler} called`);
return next.handle();
}5.2 读取自定义元数据
typescript
@SetMetadata('cache', 60)
@Get('list')
list() {}
@Injectable()
export class CacheInterceptor implements NestInterceptor {
constructor(private reflector: Reflector) {}
intercept(context: ExecutionContext, next: CallHandler) {
const ttl = this.reflector.get<number>('cache', context.getHandler());
// ttl = 60
return next.handle();
}
}六、Interceptor vs Pipe vs Guard vs Filter
| 类型 | 时机 | 用途 |
|---|---|---|
| Guard | 方法调用前 | 鉴权、限流 |
| Pipe | 参数传入前 | 转换、验证 |
| Interceptor | 调用前后 | 日志、缓存、转换 |
| ExceptionFilter | 异常抛出后 | 异常处理 |
七、组合多个 Interceptor
typescript
@Controller('users')
@UseInterceptors(LoggingInterceptor, TransformInterceptor, CacheInterceptor)
export class UserController {
@Get()
list() {
return this.userService.findAll();
}
}执行顺序:
请求 → Logging → Transform → Cache → Controller
响应 ← Logging ← Transform ← Cache ←八、全局 Interceptor
8.1 在 main.ts 注册
typescript
app.useGlobalInterceptors(new LoggingInterceptor());8.2 在模块中注册(推荐)
typescript
@Module({
providers: [
{
provide: APP_INTERCEPTOR,
useClass: LoggingInterceptor,
},
{
provide: APP_INTERCEPTOR,
useClass: TransformInterceptor,
},
],
})
export class AppModule {}九、实战:缓存拦截器
typescript
@Injectable()
export class HttpCacheInterceptor implements NestInterceptor {
constructor(private reflector: Reflector) {}
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
const cacheKey = this.getCacheKey(context);
const ttl = this.reflector.get<number>('cache-ttl', context.getHandler()) || 60;
if (!cacheKey) return next.handle();
const cached = cacheStore.get(cacheKey);
if (cached) return of(cached);
return next.handle().pipe(
tap(data => cacheStore.set(cacheKey, data, ttl)),
);
}
private getCacheKey(context: ExecutionContext): string | null {
const req = context.switchToHttp().getRequest();
if (req.method !== 'GET') return null;
return `${req.url}`;
}
}
// 自定义装饰器
export const CacheTTL = (seconds: number) => SetMetadata('cache-ttl', seconds);
// 使用
@Get('list')
@CacheTTL(120) // 缓存 2 分钟
list() {
return this.userService.findAll();
}十、实战:空值转默认值
typescript
@Injectable()
export class NullToDefaultInterceptor implements NestInterceptor {
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
return next.handle().pipe(
map(data => data ?? { code: 200, message: 'OK', data: [], timestamp: Date.now() }),
);
}
}十一、本章小结
| 操作符 | 用途 |
|---|---|
tap | 副作用 |
map | 转换值 |
timeout | 超时控制 |
catchError | 异常捕获 |
mergeMap | 异步合并 |
of() | 直接发射值 |
动手练习
- 实现一个全局 TransformInterceptor,统一响应格式
- 实现一个 LoggingInterceptor,记录请求耗时
- 用 CacheInterceptor 缓存 GET 接口
- 用 timeout 操作符实现 3 秒超时
推荐阅读
下一章:第 176 章:TypeORM 集成与实体 →