Skip to content

拦截器

继往开来:深入 NestJS 拦截器

这篇文章将带你全面了解拦截器,让你明白它是什么、如何使用,以及它与我们之前讨论过的中间件有何不同。

1. 什么是拦截器?给你的函数“套上”一个“魔法外壳”

拦截器是一个实现了 NestInterceptor 接口并使用 @Injectable() 装饰的类。

它的核心思想来源于一个非常重要的编程概念:面向切面编程 (AOP)

前置知识:什么是面向切面编程 (AOP)?

想象一下,你的应用里有很多个功能函数,比如“创建用户”、“更新文章”、“获取商品列表”。现在,你希望在每一个函数执行前后都记录日志,或者统计它们的执行时间。

最笨的办法是在每个函数开头和结尾都加上重复的日志代码。但这显然很糟糕,代码冗余且难以维护。

AOP 就是为了解决这类问题而生的。 它允许我们将这些“横切”多个功能的通用逻辑(如日志、性能监控、事务管理)抽离出来,形成一个独立的模块,这个模块就被称为“切面 (Aspect)”。 然后,AOP 会在不修改原有业务代码的情况下,将这些“切面”逻辑“织入”到需要的地方。

NestJS 的拦截器就是 AOP 思想的完美体现。 它可以让你:

  • 在目标方法执行之前和之后绑定额外的逻辑。
  • 转换从函数返回的结果。
  • 转换从函数抛出的异常。
  • 扩展函数的基本行为。
  • 在特定条件下完全重写一个函数(例如,为了实现缓存)。

把它想象成一个“魔法外壳”,你可以用它包裹任何你想增强的 Controller 方法。当请求来临时,会先进入这个“外壳”的前半部分,然后执行你真正的业务逻辑,最后再穿过“外壳”的后半部分返回给用户。

2. 创建你的第一个拦截器

每个拦截器都必须实现一个 intercept() 方法。这个方法接收两个参数:ExecutionContextCallHandler

让我们创建一个简单的日志拦截器,它会记录请求处理的耗时。

src/logging.interceptor.ts

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('在方法执行之前...'); // [1]

    const now = Date.now();
    return next
      .handle() // [2]
      .pipe(
        tap(() => console.log(`在方法执行之后... 耗时 ${Date.now() - now}ms`)), // [3]
      );
  }
}

我们来分解一下这段代码:

  1. 在方法执行之前...: 这部分代码位于 next.handle() 调用之前,所以它会在你的路由处理函数(例如 CatsController 里的方法)执行运行。
  2. next.handle(): 这是最关键的部分。调用 handle() 会执行你真正的路由处理函数。如果你不调用它,你的 Controller 方法将永远不会被执行! handle() 返回一个 Observable
  3. pipe(tap(...)): 这是利用了 RxJS 的强大功能。Observable 就像一个数据流,你可以使用各种操作符 (operators) 来处理这个流。tap() 操作符允许你在流正常结束时执行一个副作用(这里是打印日志),但它不会改变流本身。这部分代码会在路由处理函数执行完毕并返回结果运行。

核心概念解释

  • ExecutionContext (执行上下文): 和守卫 (Guard) 中的一样,它提供了关于当前执行过程的丰富信息。 你可以用它获取到将要被执行的控制器类 (context.getClass()) 和处理函数 (context.getHandler())。 这使得编写可重用的通用拦截器成为可能。
  • CallHandler: 它封装了路由处理函数的调用。它的 handle() 方法返回一个 RxJSObservable 对象,这个对象“包裹”着你路由处理函数的最终返回值。
  • RxJSObservable: 这是 NestJS 异步处理的核心。你可以把它简单理解为一个“未来的值”或“事件流”。即使你的路由处理函数是同步的 (async/await),NestJS 在内部也会将其转换为 Observable 以便拦截器可以使用。

3. 应用拦截器

和守卫、管道一样,拦截器可以应用在不同的层级:方法级、控制器级、或全局级。 我们使用 @UseInterceptors() 装饰器来应用它。

应用在单个方法上

typescript
// src/cats/cats.controller.ts
import { Controller, Get, UseInterceptors } from '@nestjs/common';
import { LoggingInterceptor } from '../logging.interceptor';

@Controller('cats')
export class CatsController {
  @Get()
  @UseInterceptors(LoggingInterceptor) // 只对这个路由生效
  findAll(): string {
    console.log('正在执行 findAll 业务逻辑...');
    return 'This action returns all cats';
  }
}

当你访问 GET /cats 时,控制台会依次输出:

在方法执行之前...
正在执行 findAll 业务逻辑...
在方法执行之后... 耗时 1ms

应用在整个控制器上

typescript
// src/cats/cats.controller.ts
import { Controller, Get, Post, UseInterceptors } from '@nestjs/common';
import { LoggingInterceptor } from '../logging.interceptor';

@Controller('cats')
@UseInterceptors(LoggingInterceptor) // 对这个控制器下的所有路由生效
export class CatsController {
  @Get()
  findAll(): string {
    return 'This action returns all cats';
  }

  @Post()
  create(): string {
    return 'This action adds a new cat';
  }
}

应用为全局拦截器

main.ts 文件中注册全局拦截器。

typescript
// src/main.ts
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { LoggingInterceptor } from './logging.interceptor';

async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  app.useGlobalInterceptors(new LoggingInterceptor()); // 注册为全局拦截器
  await app.listen(3000);
}
bootstrap();

注意:这种方式无法为拦截器注入依赖。如果需要依赖注入,你应该在根模块 (AppModule) 中通过 providers 来注册。

4. 强大的实际用例

拦截器的强大之处在于它能处理各种横切关注点。

用例一:统一响应格式 (Response Mapping)

这是一个非常常见的需求:无论后端返回什么数据,都用一个固定的结构(如 { code, data, message })包裹起来再发给前端。

src/transform.interceptor.ts

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

export interface Response<T> {
  data: T;
}

@Injectable()
export class TransformInterceptor<T> implements NestInterceptor<T, Response<T>> {
  intercept(context: ExecutionContext, next: CallHandler): Observable<Response<T>> {
    return next.handle().pipe(
      map(data => ({
        statusCode: context.switchToHttp().getResponse().statusCode,
        message: 'Success',
        data,
      })),
    );
  }
}
  • 这里我们用了 map 操作符,它会获取 Observable 流中的数据(也就是你路由处理函数的返回值 data),然后将其转换为一个新的格式。

现在,如果你有一个路由返回 ['cat1', 'cat2'],经过这个拦截器后,前端实际收到的会是:

json
{
  "statusCode": 200,
  "message": "Success",
  "data": ["cat1", "cat2"]
}

用例二:实现缓存 (Overriding the stream)

对于那些不经常变化但查询成本高的数据,我们可以用拦截器实现一个简单的缓存。

src/cache.interceptor.ts

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

// 假设我们有一个全局的缓存对象
const cache = new Map<string, any>();

@Injectable()
export class CacheInterceptor implements NestInterceptor {
  intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
    const key = context.switchToHttp().getRequest().url; // 使用 URL 作为缓存键
    if (cache.has(key)) {
      console.log('从缓存中返回...');
      return of(cache.get(key)); // 直接返回缓存的数据,不调用 handle()
    }

    console.log('首次查询,存入缓存...');
    return next.handle().pipe(
      tap(response => {
        cache.set(key, response);
      }),
    );
  }
}
```*   这个例子展示了拦截器的另一个强大能力:如果满足特定条件(缓存命中),我们可以通过返回一个全新的 `Observable` (`of(cache.get(key))`) 来**完全阻止**路由处理函数的执行。

### 5. 拦截器 vs. 中间件,到底该用谁?

这是初学者最容易困惑的地方。

| 特性 | 中间件 (Middleware) | 拦截器 (Interceptor) |
| :--- | :--- | :--- |
| **核心职责** | 请求预处理,做一些与框架耦合度低的操作。 | **AOP**,在方法执行前后添加逻辑,转换结果/异常。 |
| **访问能力** | 可以访问原始的 `req` 和 `res` 对象,但**无法**直接访问 `ExecutionContext`。 | 可以访问 `ExecutionContext`,能获取到要执行的类和方法。 |
| **执行时机** | 在**守卫 (Guard)** 之前执行。 | 在**守卫 (Guard)** 之后,**管道 (Pipe)** 之前和之后执行。 |
| **响应处理** | 理论上可以,但比较笨拙。主要关注请求处理。 | **核心能力**。可以轻松地通过 `RxJS` 操作符修改响应。 |
| **典型用例** | CORS、Helmet 安全头、Cookie 解析、原始请求日志。 | 统一响应格式、缓存、性能监控、转换响应数据。 |

**简单总结一下选择标准**:

*   当你需要处理最原始的 `request` 和 `response` 对象,或者需要引入 Express/Fastify 的生态中的一些中间件时,使用**中间件**。
*   当你需要的功能与某个具体的 `Controller` 或 `Method` 的执行前后逻辑紧密相关,尤其是需要**修改返回结果**时,**拦截器**是你的不二之选。

### 总结

拦截器为 NestJS 应用提供了一种强大而优雅的方式来处理横切关注点。通过利用 AOP 和 RxJS 的能力,你可以编写出高度可复用且功能强大的模块,用于日志记录、数据转换、缓存等多种场景。

理解拦截器与中间件、守卫、管道之间的区别和执行顺序,是掌握 NestJS 请求生命周期的关键。希望这篇文章能帮助你扫清障碍,在 NestJS 的学习道路上更进一步!