Skip to content
第 175 / 250 章Node⏱ 10 分钟阅读

第 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()直接发射值

动手练习

  1. 实现一个全局 TransformInterceptor,统一响应格式
  2. 实现一个 LoggingInterceptor,记录请求耗时
  3. 用 CacheInterceptor 缓存 GET 接口
  4. 用 timeout 操作符实现 3 秒超时

推荐阅读


下一章:第 176 章:TypeORM 集成与实体

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