Skip to main content

拦截器

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

Nest.js 拦截器

拦截器具有一组有用的功能,受到面向切面编程(AOP)技术的启发。 它们使以下操作成为可能:

  • 在方法执行之前/之后绑定额外的逻辑
  • 转换从函数返回的结果
  • 转换从函数抛出的异常
  • 扩展基本函数行为
  • 根据特定条件(例如,用于缓存目的)完全覆盖函数

基础知识

每个拦截器都实现了 intercept() 方法该方法接受两个参数第一个ExecutionContext 实例(与守卫的对象完全相同)。 ExecutionContext 继承自 ArgumentsHost。 我们在异常过滤器章节中已经看到过 ArgumentsHost。 在那里,我们看到它是对传递给原始处理程序的参数的包装,根据应用程序类型包含不同的参数数组。 您可以回到异常过滤器章节了解更多关于这个主题的信息。

执行上下文

通过扩展 ArgumentsHostExecutionContext 还添加了几个新的辅助方法,提供有关当前执行过程的额外详细信息。 这些详细信息对于构建更通用的拦截器,在广泛的控制器、方法和执行上下文中工作,是有帮助的。 在这里了解有关ExecutionContext的更多信息。

调用处理程序

第二个参数CallHandlerCallHandler 接口实现了 handle() 方法, 您可以在拦截器的某个点使用它来调用路由处理程序方法。 如果在 intercept() 方法的实现中没有调用 handle() 方法,路由处理程序方法将根本不会被执行。

这种方法意味着 intercept() 方法有效地包装了请求/响应流。 因此,您可以在执行最终路由处理程序之前和之后实现自定义逻辑。 很明显,您可以在 intercept() 方法中编写在调用 handle() 之前执行的代码, 但是如何影响之后发生的事情呢?因为 handle() 方法返回一个 Observable, 我们可以使用强大的 RxJS 操作符来进一步操作响应。使用面向切面编程的术语, 路由处理程序的调用(即调用 handle())被称为切点(Pointcut), 表明这是我们插入附加逻辑的点

例如,考虑一下传入的 POST /cats 请求。 此请求将发送到 CatsController 内定义的 create() 处理程序。 如果在任何地方调用了不调用 handle() 方法的拦截器,create() 方法将不会被执行。 一旦调用了 handle()(并且其 Observable 已经返回),将触发 create() 处理程序。 并且一旦通过 Observable 收到响应流,就可以在流上执行其他操作,并将最终结果返回给调用方。

切面拦截

我们首先将看一下使用拦截器记录用户交互的用例(例如,存储用户调用、异步调度事件或计算时间戳)。 下面展示了一个简单的 LoggingInterceptor

logging.interceptor.ts
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`)),
);
}
}
note

NestInterceptor<T, R>是一个泛型接口,其中T表示Observable<T>(支持响应流)的类型, 而R是由Observable<R>包装的值的类型。

tip

拦截器,就像控制器、提供者、守卫等一样,可以通过其构造函数注入依赖项

由于handle()返回一个 RxJS Observable,我们可以选择广泛使用操作符来操作流。 在上面的示例中,我们使用了tap()操作符,它在可观察流的正常或异常终止时调用我们的匿名日志函数, 但不干扰响应周期以外的任何操作。

绑定拦截器

为了设置拦截器, 我们使用了从@nestjs/common包中导入的@UseInterceptors()装饰器。 与管道和守卫一样,拦截器可以是控制器范围、方法范围或全局范围

cats.controller.ts
@UseInterceptors(LoggingInterceptor)
export class CatsController {}
tip

@nestjs/common导入@UseInterceptors()

使用上述结构,CatsController中定义的每个路由处理程序都将使用 LoggingInterceptor。 当有人调用 GET /cats 端点时,您将在标准输出中看到以下输出:

Before...
After... 1ms

请注意,我们传递了 LoggingInterceptor 类型(而不是一个实例), 将实例化的责任留给框架并启用依赖注入。 与管道、守卫和异常过滤器一样,我们也可以传递一个就地(place)实例

cats.controller.ts
@UseInterceptors(new LoggingInterceptor())
export class CatsController {}

如上所述,上述构造将拦截器附加到此控制器声明的每个处理程序。 如果我们想将拦截器的范围限制为单个方法,我们只需在方法级别应用装饰器

为了设置全局拦截器,我们使用 Nest 应用程序实例的 useGlobalInterceptors() 方法:

const app = await NestFactory.create(AppModule);
app.useGlobalInterceptors(new LoggingInterceptor());

全局拦截器在整个应用程序中使用,对每个控制器和每个路由处理程序都有效。 在依赖注入方面,从任何模块外部注册的全局拦截器(如上例中使用 useGlobalInterceptors()) 无法注入依赖项,因为这是在任何模块的上下文之外完成的。 为了解决这个问题,您可以直接从任何模块中使用以下结构设置拦截器:

app.module.ts
import { Module } from '@nestjs/common';
import { APP_INTERCEPTOR } from '@nestjs/core';

@Module({
providers: [
{
provide: APP_INTERCEPTOR,
useClass: LoggingInterceptor,
},
],
})
export class AppModule {}
tip

使用这种方法进行拦截器的依赖注入时,请注意无论在哪个模块中使用此结构,拦截器实际上都是全局的。 应该在哪里进行这个操作?选择定义拦截器的模块(如上例中的LoggingInterceptor)。 此外,useClass不是处理自定义提供程序注册的唯一方式。 在这里了解更多信息。

响应映射

我们已经知道handle()返回一个Observable。 该流包含从路由处理程序返回的值,因此我们可以使用 RxJS 的map()操作符轻松地对其进行变异。

warning

响应映射功能不能与库特定的响应策略一起使用(直接使用@Res()对象是禁止的)。

让我们创建TransformInterceptor,它将以一种微不足道的方式修改每个响应,以演示这个过程。 它将使用 RxJS 的map()操作符将响应对象分配给新创建的对象的data属性,并将新对象返回给客户端。

transform.interceptor.ts
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 => ({ data })));
}
}
tip

Nest 拦截器支持同步和异步的 intercept() 方法。如果需要,您可以简单地将方法切换为异步。

使用上述结构,当有人调用GET /cats端点时,响应将如下所示(假设路由处理程序返回一个空数组 []):

{
"data": []
}

拦截器在创建可在整个应用程序中重复使用的解决方案方面具有很大的价值。 例如,假设我们需要将每次出现的空值(null)转换为空字符串 '',我们可以使用一行代码来实现, 并将拦截器全局绑定,以便它将自动被每个注册的处理程序使用。

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

@Injectable()
export class ExcludeNullInterceptor implements NestInterceptor {
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
return next
.handle()
.pipe(map(value => value === null ? '': value));
}
}

异常映射

另一个有趣的用例是利用 RxJS 的 catchError() 操作符来覆盖抛出的异常:

errors.interceptor.ts
import {
Injectable,
NestInterceptor,
ExecutionContext,
BadGatewayException,
CallHandler,
} 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(err => throwError(() => new BadGatewayException()))
);
}
}

流覆盖

有几个原因可能导致我们有时希望完全阻止调用处理程序并返回不同的值。 一个明显的例子是实现缓存以提高响应时间。 让我们来看一个简单的缓存拦截器,它从缓存返回响应。 在实际示例中,我们可能要考虑其他因素,如 TTL、缓存失效、缓存大小等,但这超出了本讨论的范围。 这里我们提供一个演示主要概念的基本示例。

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

@Injectable()
export class CacheInterceptor implements NestInterceptor {
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
const isCached = true;
if (isCached) {
return of([]);
}
return next.handle();
}
}

我们的 CacheInterceptor 有一个硬编码的 isCached 变量和一个硬编码的响应 []。 要注意的关键点是,我们在这里返回一个新的流,由 RxJS 的 of() 操作符创建,因此根本不会调用路由处理程序。 当有人调用使用 CacheInterceptor 的端点时,响应(一个硬编码的空数组)将立即返回。 为了创建一个通用的解决方案,您可以利用Reflector并创建一个自定义装饰器。 在守卫章节中对Reflector进行了详细描述。

更多操作符

使用 RxJS 操作符操纵流的可能性为我们提供了许多功能。让我们考虑另一个常见的用例。 想象一下,您希望处理路由请求的超时情况。 当端点在一段时间后没有返回任何内容时,您希望以错误响应终止。 以下结构使这成为可能:

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

@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) {
return throwError(() => new RequestTimeoutException());
}
return throwError(() => err);
})
)
}
}

5秒后,请求处理将被取消。您还可以在抛出 RequestTimeoutException 之前添加自定义逻辑(例如释放资源)。