NestJS拦截器 (Interceptors)

学习目标

  • 理解拦截器在NestJS中的作用和地位
  • 掌握拦截器的创建和基本使用方法
  • 学会实现响应映射和请求预处理
  • 理解如何使用拦截器处理异常和实现缓存
  • 能够应用拦截器优化请求处理流程

核心知识点

1. 拦截器概念

拦截器是NestJS中用于处理请求和响应的组件,它们在请求处理管道中的位置如下:

  1. 客户端发送请求
  2. 中间件处理请求
  3. 守卫验证权限
  4. 管道处理数据
  5. 控制器处理请求
  6. 服务执行业务逻辑
  7. 拦截器处理响应
  8. 异常过滤器处理异常
  9. 响应返回给客户端

拦截器的主要作用:

  • 响应映射:转换或修改响应数据
  • 请求预处理:在控制器处理前修改请求数据
  • 异常处理:捕获和处理异常
  • 缓存:缓存响应结果
  • 日志记录:记录请求和响应信息
  • 性能监控:测量请求处理时间

2. 拦截器的创建

创建拦截器需要实现NestInterceptor接口:

import { Injectable, NestInterceptor, ExecutionContext, CallHandler } from '@nestjs/common';
import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';

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

3. 拦截器的应用

控制器级别

使用@UseInterceptors()装饰器在控制器级别应用拦截器:

import { Controller, Get, UseInterceptors } from '@nestjs/common';
import { TransformInterceptor } from './transform.interceptor';

@Controller('cats')
@UseInterceptors(TransformInterceptor)
export class CatsController {
  @Get()
  findAll() {
    return [{ name: 'Cat 1' }, { name: 'Cat 2' }];
  }
}

方法级别

使用@UseInterceptors()装饰器在方法级别应用拦截器:

import { Controller, Get, UseInterceptors } from '@nestjs/common';
import { TransformInterceptor } from './transform.interceptor';

@Controller('cats')
export class CatsController {
  @Get()
  @UseInterceptors(TransformInterceptor)
  findAll() {
    return [{ name: 'Cat 1' }, { name: 'Cat 2' }];
  }
}

全局级别

在应用程序级别注册全局拦截器:

// main.ts
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { TransformInterceptor } from './transform.interceptor';

async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  app.useGlobalInterceptors(new TransformInterceptor());
  await app.listen(3000);
}
bootstrap();

4. 响应映射

使用拦截器转换响应数据:

// transform.interceptor.ts
import { Injectable, NestInterceptor, ExecutionContext, CallHandler } from '@nestjs/common';
import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';

@Injectable()
export class TransformInterceptor implements NestInterceptor {
  intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
    return next.handle().pipe(
      map(data => ({
        data,
        statusCode: context.switchToHttp().getResponse().statusCode,
        timestamp: new Date().toISOString(),
        path: context.switchToHttp().getRequest().url,
      })),
    );
  }
}

5. 请求预处理

使用拦截器在控制器处理前修改请求数据:

// logging.interceptor.ts
import { Injectable, NestInterceptor, ExecutionContext, CallHandler, Logger } from '@nestjs/common';
import { Observable } from 'rxjs';
import { tap } from 'rxjs/operators';

@Injectable()
export class LoggingInterceptor implements NestInterceptor {
  private readonly logger = new Logger(LoggingInterceptor.name);

  intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
    const now = Date.now();
    const request = context.switchToHttp().getRequest();
    
    this.logger.log(
      `[${request.method}] ${request.url} - Request started`,
    );

    return next.handle().pipe(
      tap(() => {
        const response = context.switchToHttp().getResponse();
        const responseTime = Date.now() - now;
        
        this.logger.log(
          `[${request.method}] ${request.url} - Response ${response.statusCode} - ${responseTime}ms`,
        );
      }),
    );
  }
}

6. 异常处理

使用拦截器捕获和处理异常:

// error.interceptor.ts
import { Injectable, NestInterceptor, ExecutionContext, CallHandler, HttpStatus } from '@nestjs/common';
import { Observable, throwError } from 'rxjs';
import { catchError } from 'rxjs/operators';

@Injectable()
export class ErrorInterceptor implements NestInterceptor {
  intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
    return next.handle().pipe(
      catchError(error => {
        // 统一处理异常
        const response = context.switchToHttp().getResponse();
        
        // 设置默认状态码
        const statusCode = error.status || HttpStatus.INTERNAL_SERVER_ERROR;
        
        // 构建错误响应
        const errorResponse = {
          statusCode,
          message: error.message || 'Internal server error',
          timestamp: new Date().toISOString(),
          path: context.switchToHttp().getRequest().url,
        };
        
        // 设置响应状态码
        response.status(statusCode);
        
        // 返回错误响应
        return throwError(() => errorResponse);
      }),
    );
  }
}

7. 缓存实现

使用拦截器实现简单的缓存:

// cache.interceptor.ts
import { Injectable, NestInterceptor, ExecutionContext, CallHandler } from '@nestjs/common';
import { Observable, of } from 'rxjs';
import { tap } from 'rxjs/operators';

@Injectable()
export class CacheInterceptor implements NestInterceptor {
  private readonly cache = new Map<string, any>();

  intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
    const request = context.switchToHttp().getRequest();
    const cacheKey = `${request.method}:${request.url}`;
    
    // 检查缓存
    if (this.cache.has(cacheKey)) {
      return of(this.cache.get(cacheKey));
    }

    return next.handle().pipe(
      tap(data => {
        // 缓存响应
        this.cache.set(cacheKey, data);
        
        // 设置缓存过期时间(示例:5分钟)
        setTimeout(() => {
          this.cache.delete(cacheKey);
        }, 5 * 60 * 1000);
      }),
    );
  }
}

8. 拦截器的依赖注入

拦截器可以通过依赖注入系统注入其他服务:

// cache.interceptor.ts
import { Injectable, NestInterceptor, ExecutionContext, CallHandler } from '@nestjs/common';
import { Observable, of } from 'rxjs';
import { tap } from 'rxjs/operators';
import { CacheService } from './cache.service';

@Injectable()
export class CacheInterceptor implements NestInterceptor {
  constructor(private cacheService: CacheService) {}

  intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
    const request = context.switchToHttp().getRequest();
    const cacheKey = `${request.method}:${request.url}`;
    
    // 检查缓存
    const cachedData = this.cacheService.get(cacheKey);
    if (cachedData) {
      return of(cachedData);
    }

    return next.handle().pipe(
      tap(data => {
        // 缓存响应
        this.cacheService.set(cacheKey, data, 300); // 5分钟过期
      }),
    );
  }
}

9. 拦截器链

可以同时应用多个拦截器,它们会按照注册顺序执行:

@Controller('protected')
@UseInterceptors(LoggingInterceptor, TransformInterceptor, CacheInterceptor)
export class ProtectedController {
  // 控制器方法
}

10. 上下文类型

拦截器可以处理不同类型的上下文,不仅仅是HTTP请求:

import { Injectable, NestInterceptor, ExecutionContext, CallHandler } from '@nestjs/common';
import { Observable } from 'rxjs';
import { tap } from 'rxjs/operators';

@Injectable()
export class WsLoggingInterceptor 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`)),
    );
  }
}

实践案例分析

案例:响应格式化和日志记录

需求分析

我们需要创建一个拦截器系统,包括:

  • 统一的响应格式
  • 详细的请求和响应日志
  • 性能监控(响应时间)
  • 错误处理和日志记录
  • 可配置的拦截器链

实现步骤

  1. 创建响应格式化拦截器
  2. 创建日志记录拦截器
  3. 创建错误处理拦截器
  4. 配置拦截器链
  5. 测试拦截器效果

代码实现

1. 创建响应格式化拦截器
// transform.interceptor.ts
import { Injectable, NestInterceptor, ExecutionContext, CallHandler } from '@nestjs/common';
import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';

@Injectable()
export class TransformInterceptor implements NestInterceptor {
  intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
    return next.handle().pipe(
      map(data => ({
        success: true,
        data,
        metadata: {
          statusCode: context.switchToHttp().getResponse().statusCode,
          timestamp: new Date().toISOString(),
          path: context.switchToHttp().getRequest().url,
        },
      })),
    );
  }
}
2. 创建日志记录拦截器
// logging.interceptor.ts
import { Injectable, NestInterceptor, ExecutionContext, CallHandler, Logger } from '@nestjs/common';
import { Observable } from 'rxjs';
import { tap } from 'rxjs/operators';

@Injectable()
export class LoggingInterceptor implements NestInterceptor {
  private readonly logger = new Logger(LoggingInterceptor.name);

  intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
    const now = Date.now();
    const request = context.switchToHttp().getRequest();
    const method = request.method;
    const url = request.url;
    const ip = request.ip;
    
    this.logger.log(`[${method}] ${url} - Request from ${ip}`, {
      method,
      url,
      ip,
      headers: request.headers,
      body: request.body,
      query: request.query,
    });

    return next.handle().pipe(
      tap(() => {
        const response = context.switchToHttp().getResponse();
        const statusCode = response.statusCode;
        const responseTime = Date.now() - now;
        
        this.logger.log(`[${method}] ${url} - Response ${statusCode} - ${responseTime}ms`, {
          method,
          url,
          statusCode,
          responseTime,
        });
      }),
    );
  }
}
3. 创建错误处理拦截器
// error.interceptor.ts
import { Injectable, NestInterceptor, ExecutionContext, CallHandler, HttpStatus, Logger } from '@nestjs/common';
import { Observable, throwError } from 'rxjs';
import { catchError } from 'rxjs/operators';

@Injectable()
export class ErrorInterceptor implements NestInterceptor {
  private readonly logger = new Logger(ErrorInterceptor.name);

  intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
    return next.handle().pipe(
      catchError(error => {
        const request = context.switchToHttp().getRequest();
        const method = request.method;
        const url = request.url;
        
        // 记录错误日志
        this.logger.error(`[${method}] ${url} - Error: ${error.message}`, error.stack, {
          method,
          url,
          error: {
            message: error.message,
            status: error.status,
            stack: error.stack,
          },
        });

        // 构建错误响应
        const statusCode = error.status || HttpStatus.INTERNAL_SERVER_ERROR;
        const errorResponse = {
          success: false,
          error: {
            statusCode,
            message: error.message || 'Internal server error',
            timestamp: new Date().toISOString(),
            path: url,
          },
        };

        return throwError(() => errorResponse);
      }),
    );
  }
}
4. 创建测试控制器
// cats.controller.ts
import { Controller, Get, Post, Body, Param, HttpException, HttpStatus } from '@nestjs/common';

@Controller('cats')
export class CatsController {
  @Get()
  findAll() {
    return [
      { id: 1, name: 'Cat 1', age: 2 },
      { id: 2, name: 'Cat 2', age: 3 },
    ];
  }

  @Get(':id')
  findOne(@Param('id') id: string) {
    if (id === '999') {
      throw new HttpException('Cat not found', HttpStatus.NOT_FOUND);
    }
    
    return { id: parseInt(id), name: `Cat ${id}`, age: 2 };
  }

  @Post()
  create(@Body() cat: { name: string; age: number }) {
    if (!cat.name) {
      throw new HttpException('Name is required', HttpStatus.BAD_REQUEST);
    }
    
    return { id: 3, ...cat };
  }
}
5. 配置全局拦截器
// main.ts
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { LoggingInterceptor } from './logging.interceptor';
import { TransformInterceptor } from './transform.interceptor';
import { ErrorInterceptor } from './error.interceptor';

async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  
  // 注册全局拦截器(顺序很重要)
  app.useGlobalInterceptors(
    new LoggingInterceptor(), // 首先记录请求开始
    new TransformInterceptor(), // 然后格式化响应
    new ErrorInterceptor(), // 最后处理错误
  );
  
  await app.listen(3000);
}
bootstrap();
6. 创建模块
// cats.module.ts
import { Module } from '@nestjs/common';
import { CatsController } from './cats.controller';

@Module({
  controllers: [CatsController],
})
export class CatsModule {}
// app.module.ts
import { Module } from '@nestjs/common';
import { CatsModule } from './cats/cats.module';
import { LoggingInterceptor } from './logging.interceptor';
import { TransformInterceptor } from './transform.interceptor';
import { ErrorInterceptor } from './error.interceptor';

@Module({
  imports: [CatsModule],
  providers: [LoggingInterceptor, TransformInterceptor, ErrorInterceptor],
})
export class AppModule {}

测试结果

1. 正常请求 - 获取所有猫咪

请求

GET /cats

响应

{
  "success": true,
  "data": [
    { "id": 1, "name": "Cat 1", "age": 2 },
    { "id": 2, "name": "Cat 2", "age": 3 }
  ],
  "metadata": {
    "statusCode": 200,
    "timestamp": "2023-10-01T10:00:00.000Z",
    "path": "/cats"
  }
}

日志

[LOG] [GET] /cats - Request from ::1 {
  "method": "GET",
  "url": "/cats",
  "ip": "::1",
  "headers": { /* 请求头 */ },
  "body": {},
  "query": {}
}
[LOG] [GET] /cats - Response 200 - 5ms {
  "method": "GET",
  "url": "/cats",
  "statusCode": 200,
  "responseTime": 5
}
2. 正常请求 - 创建猫咪

请求

POST /cats
Content-Type: application/json

{
  "name": "Cat 3",
  "age": 1
}

响应

{
  "success": true,
  "data": {
    "id": 3,
    "name": "Cat 3",
    "age": 1
  },
  "metadata": {
    "statusCode": 201,
    "timestamp": "2023-10-01T10:00:00.000Z",
    "path": "/cats"
  }
}

日志

[LOG] [POST] /cats - Request from ::1 {
  "method": "POST",
  "url": "/cats",
  "ip": "::1",
  "headers": { /* 请求头 */ },
  "body": {
    "name": "Cat 3",
    "age": 1
  },
  "query": {}
}
[LOG] [POST] /cats - Response 201 - 8ms {
  "method": "POST",
  "url": "/cats",
  "statusCode": 201,
  "responseTime": 8
}
3. 错误请求 - 猫咪不存在

请求

GET /cats/999

响应

{
  "success": false,
  "error": {
    "statusCode": 404,
    "message": "Cat not found",
    "timestamp": "2023-10-01T10:00:00.000Z",
    "path": "/cats/999"
  }
}

日志

[LOG] [GET] /cats/999 - Request from ::1 {
  "method": "GET",
  "url": "/cats/999",
  "ip": "::1",
  "headers": { /* 请求头 */ },
  "body": {},
  "query": {}
}
[ERROR] [GET] /cats/999 - Error: Cat not found {
  "method": "GET",
  "url": "/cats/999",
  "error": {
    "message": "Cat not found",
    "status": 404,
    "stack": "/* 错误堆栈 */"
  }
}
4. 错误请求 - 缺少必填字段

请求

POST /cats
Content-Type: application/json

{
  "age": 1
}

响应

{
  "success": false,
  "error": {
    "statusCode": 400,
    "message": "Name is required",
    "timestamp": "2023-10-01T10:00:00.000Z",
    "path": "/cats"
  }
}

日志

[LOG] [POST] /cats - Request from ::1 {
  "method": "POST",
  "url": "/cats",
  "ip": "::1",
  "headers": { /* 请求头 */ },
  "body": {
    "age": 1
  },
  "query": {}
}
[ERROR] [POST] /cats - Error: Name is required {
  "method": "POST",
  "url": "/cats",
  "error": {
    "message": "Name is required",
    "status": 400,
    "stack": "/* 错误堆栈 */"
  }
}

代码解析

  1. 响应格式化拦截器

    • 统一了成功响应的格式,包含successdatametadata字段
    • 从上下文中获取响应状态码、请求路径等信息
  2. 日志记录拦截器

    • 记录了请求的详细信息,包括方法、URL、IP、请求头、请求体和查询参数
    • 记录了响应的状态码和处理时间
    • 使用NestJS内置的Logger类,支持不同级别的日志
  3. 错误处理拦截器

    • 捕获和记录所有异常
    • 统一了错误响应的格式,包含successerror字段
    • 确保错误信息的一致性和完整性
  4. 拦截器链

    • 按照合理的顺序注册拦截器:先记录请求,然后处理响应,最后处理错误
    • 确保每个拦截器都能正确执行
  5. 测试控制器

    • 提供了正常和错误的测试场景
    • 模拟了不同类型的错误,验证错误处理拦截器的效果

互动思考问题

  1. 思考:拦截器和中间件的区别是什么?它们各自的使用场景是什么?

  2. 讨论:在实际应用中,如何设计一个高效的缓存拦截器?需要考虑哪些因素?

  3. 实践:尝试创建一个性能监控拦截器,记录每个请求的处理时间和资源使用情况。

  4. 挑战:如何实现一个基于Redis的分布式缓存拦截器,支持多实例共享缓存?

  5. 扩展:了解NestJS的@nestjs/throttler模块,思考它如何与拦截器配合使用来实现请求限流。

小结

本集我们学习了NestJS拦截器的核心概念和使用方法,包括:

  • 拦截器的基本概念和作用
  • 拦截器的创建和注册方法
  • 响应映射的实现
  • 请求预处理和日志记录
  • 异常处理的实现
  • 缓存的简单实现
  • 拦截器链的使用
  • 不同上下文类型的处理

通过实践案例,我们创建了一个完整的响应格式化和日志记录系统,展示了如何使用拦截器优化请求处理流程并提供一致的响应格式。拦截器是NestJS处理请求和响应的重要组件,它使得响应处理更加灵活和可管理。

在下一集中,我们将学习NestJS的自定义装饰器(Custom Decorators),了解如何创建和使用自定义装饰器来增强代码的可读性和可维护性。

« 上一篇 NestJS守卫 (Guards) 下一篇 » NestJS自定义装饰器 (Custom Decorators)