Skip to content

什么是拦截器?🤔 ​

拦截器(Interceptor)是一个使用 @Injectable() 装饰器注解的类,它实现了 NestInterceptor 接口。拦截器具有一系列有用的功能,这些功能受面向切面编程(AOP)技术的启发。它们可以:

  • 在函数执行之前/之后绑定额外的逻辑
  • 转换函数返回的结果
  • 转换函数抛出的异常
  • 扩展基本函数行为
  • 根据特定条件完全重写函数

Express vs NestJS 对比 🆚 ​

让我们看看在传统 Express 和 NestJS 中如何处理响应转换:

Express 写法:

javascript
// 响应包装中间件
const responseWrapper = (req, res, next) => {
  const originalJson = res.json;
  res.json = function(data) {
    return originalJson.call(this, {
      code: 200,
      data,
      message: 'success'
    });
  };
  next();
};

// 日志中间件
const logger = (req, res, next) => {
  const start = Date.now();
  res.on('finish', () => {
    const duration = Date.now() - start;
    console.log(`${req.method} ${req.url} ${duration}ms`);
  });
  next();
};

app.use(responseWrapper);
app.use(logger);

NestJS 写法:

typescript
@Injectable()
export class TransformInterceptor implements NestInterceptor {
  intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
    return next.handle().pipe(
      map(data => ({
        code: 200,
        data,
        message: 'success'
      }))
    );
  }
}

@Injectable()
export class LoggingInterceptor implements NestInterceptor {
  intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
    const start = Date.now();
    const request = context.switchToHttp().getRequest();

    return next.handle().pipe(
      tap(() => {
        const duration = Date.now() - start;
        console.log(`${request.method} ${request.url} ${duration}ms`);
      })
    );
  }
}

拦截器的应用场景 🎯 ​

1. 响应数据转换 ​

typescript
@Injectable()
export class ResponseTransformInterceptor implements NestInterceptor {
  intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
    return next.handle().pipe(
      map(data => {
        const response = context.switchToHttp().getResponse();
        return {
          code: response.statusCode,
          data,
          message: 'success',
          timestamp: new Date().toISOString()
        };
      })
    );
  }
}

// 使用拦截器
@Controller('users')
@UseInterceptors(ResponseTransformInterceptor)
export class UsersController {
  @Get()
  findAll() {
    return this.usersService.findAll();
  }
}

2. 缓存拦截器 ​

typescript
@Injectable()
export class CacheInterceptor implements NestInterceptor {
  constructor(private cacheManager: Cache) {}

  async intercept(context: ExecutionContext, next: CallHandler): Promise<Observable<any>> {
    const request = context.switchToHttp().getRequest();
    const cacheKey = request.url;
    const cachedResponse = await this.cacheManager.get(cacheKey);

    if (cachedResponse) {
      return of(cachedResponse);
    }

    return next.handle().pipe(
      tap(response => {
        this.cacheManager.set(cacheKey, response, { ttl: 60 });
      })
    );
  }
}

3. 超时处理 ​

typescript
@Injectable()
export class TimeoutInterceptor implements NestInterceptor {
  intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
    return next.handle().pipe(
      timeout(5000),
      catchError(err => {
        if (err instanceof TimeoutError) {
          throw new RequestTimeoutException();
        }
        throw err;
      }),
    );
  }
}

4. 文件上传进度 ​

typescript
@Injectable()
export class FileUploadInterceptor implements NestInterceptor {
  intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
    const request = context.switchToHttp().getRequest();
    
    if (request.file) {
      const totalSize = request.file.size;
      let uploadedSize = 0;

      request.file.on('data', (chunk) => {
        uploadedSize += chunk.length;
        const progress = (uploadedSize / totalSize) * 100;
        console.log(`Upload progress: ${progress}%`);
      });
    }

    return next.handle();
  }
}

高级用法 🚀 ​

1. 异常映射 ​

typescript
@Injectable()
export class ErrorsInterceptor implements NestInterceptor {
  intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
    return next
      .handle()
      .pipe(
        catchError(err => {
          if (err instanceof HttpException) {
            return throwError(() => err);
          }
          return throwError(() => new InternalServerErrorException({
            message: '服务器内部错误',
            originalError: err.message
          }));
        }),
      );
  }
}

2. 流数据处理 ​

typescript
@Injectable()
export class StreamInterceptor implements NestInterceptor {
  intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
    return next.handle().pipe(
      map(data => {
        if (Array.isArray(data)) {
          return from(data).pipe(
            map(item => this.transformItem(item)),
            toArray()
          );
        }
        return this.transformItem(data);
      }),
    );
  }

  private transformItem(item: any) {
    // 转换逻辑
    return item;
  }
}

3. 条件拦截 ​

typescript
@Injectable()
export class ConditionalInterceptor implements NestInterceptor {
  intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
    const request = context.switchToHttp().getRequest();
    const shouldIntercept = request.query.transform === 'true';

    if (shouldIntercept) {
      return next.handle().pipe(
        map(data => this.transform(data))
      );
    }

    return next.handle();
  }

  private transform(data: any) {
    // 转换逻辑
    return data;
  }
}

实战示例 💡 ​

让我们创建一个完整的日志系统:

typescript
// interceptors/logging.interceptor.ts
@Injectable()
export class LoggingInterceptor implements NestInterceptor {
  constructor(
    private readonly logger: Logger,
    private readonly configService: ConfigService
  ) {}

  intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
    const request = context.switchToHttp().getRequest();
    const method = request.method;
    const url = request.url;
    const now = Date.now();
    const requestId = uuid();

    // 记录请求信息
    this.logger.log({
      type: 'request',
      requestId,
      method,
      url,
      body: request.body,
      query: request.query,
      headers: this.filterSensitiveHeaders(request.headers)
    });

    return next.handle().pipe(
      tap({
        next: (data) => {
          // 记录响应信息
          const duration = Date.now() - now;
          this.logger.log({
            type: 'response',
            requestId,
            duration,
            statusCode: context.switchToHttp().getResponse().statusCode,
            data: this.shouldLogResponse(url) ? data : '[FILTERED]'
          });
        },
        error: (error) => {
          // 记录错误信息
          const duration = Date.now() - now;
          this.logger.error({
            type: 'error',
            requestId,
            duration,
            error: {
              name: error.name,
              message: error.message,
              stack: this.configService.get('NODE_ENV') === 'development' 
                ? error.stack 
                : undefined
            }
          });
        }
      })
    );
  }

  private filterSensitiveHeaders(headers: any) {
    const sensitiveHeaders = ['authorization', 'cookie'];
    return Object.keys(headers).reduce((acc, key) => {
      if (sensitiveHeaders.includes(key.toLowerCase())) {
        acc[key] = '[FILTERED]';
      } else {
        acc[key] = headers[key];
      }
      return acc;
    }, {});
  }

  private shouldLogResponse(url: string): boolean {
    const excludePaths = ['/health', '/metrics'];
    return !excludePaths.some(path => url.startsWith(path));
  }
}

最佳实践 ✨ ​

1. 合理使用全局拦截器 ​

typescript
// main.ts
async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  
  app.useGlobalInterceptors(
    new ClassSerializerInterceptor(app.get(Reflector)),
    new ResponseTransformInterceptor(),
    new LoggingInterceptor()
  );
  
  await app.listen(3000);
}

2. 拦截器组合 ​

typescript
const CommonInterceptors = applyDecorators(
  UseInterceptors(
    CacheInterceptor,
    TimeoutInterceptor,
    LoggingInterceptor
  )
);

@Controller('users')
@CommonInterceptors
export class UsersController {}

3. 性能考虑 ​

typescript
@Injectable()
export class PerformanceInterceptor implements NestInterceptor {
  private readonly logger = new Logger('Performance');
  private readonly threshold = 1000; // 1秒

  intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
    const start = Date.now();
    const request = context.switchToHttp().getRequest();

    return next.handle().pipe(
      tap(() => {
        const duration = Date.now() - start;
        if (duration > this.threshold) {
          this.logger.warn(
            `Slow request detected: ${request.method} ${request.url} ${duration}ms`
          );
        }
      })
    );
  }
}

总结 📝 ​

通过这篇文章,我们深入学习了:

  1. 拦截器的基本概念和作用
  2. 不同类型的拦截器实现
  3. 拦截器的高级用法
  4. 实际项目中的应用场景
  5. 性能和最佳实践考虑

拦截器是 NestJS 中非常强大的功能,它可以帮助我们:

  • 统一处理响应格式
  • 实现日志记录
  • 处理缓存
  • 处理超时
  • 转换响应数据
  • 处理异常

下一篇文章,我们将探讨 NestJS 的异常过滤器(Exception Filter)。

相关资源 📚 ​