Nest.js 拦截器
Nest.js 拦截器是一个用 @Injectable()
装饰器注解的类,它实现了 NestInterceptor
接口。
拦截器具有一组受面向方面编程 (AOP) 技术启发的有用功能。 它们可以:
- 在方法执行之前/之后绑定额外的逻辑
- 转换从函数返回的结果
- 转换函数抛出的异常
- 扩展基本功能行为
- 根据特定条件完全覆盖函数(例如,出于缓存目的)
每个拦截器都实现了 intercept()
方法,该方法接受两个参数。 第一个是 ExecutionContext
实例(与Guard完全相同的对象)。 ExecutionContext 继承自 ArgumentsHost。 我们之前在异常过滤器章节中看到了 ArgumentsHost。 在那里,我们看到它是传递给原始处理程序的参数的包装器,并且包含基于应用程序类型的不同参数数组。 我们可以参考异常过滤器来获取有关此主题的更多信息。
通过扩展 ArgumentsHost,ExecutionContext 还添加了几个新的帮助方法,这些方法提供了有关当前执行过程的更多详细信息。 这些细节有助于构建更通用的拦截器,这些拦截器可以跨广泛的控制器、方法和执行上下文工作。
调用处理程序
第二个参数是 CallHandler
。如果不手动调用 handle()
方法,则主处理程序根本不会进行求值。这是什么意思?基本上,CallHandler是一个包装执行流的对象,因此推迟了最终的处理程序执行。
这种方法意味着 intercept() 方法有效地封装了请求/响应流。 因此,我们可以在执行最终路由处理程序之前和之后实现自定义逻辑。 很明显,我们可以在调用 handler()
之前执行的 intercept()
方法中编写代码,但是我们如何影响之后发生的事情呢? 因为 handle() 方法返回一个 Observable,我们可以使用强大的 RxJS 操作符来进一步操作响应。 使用面向方面的编程术语,路由处理程序的调用(即调用 handler() )称为切入点,表明它是插入附加逻辑的点。
比方说,有人提出了 POST /cats
请求。此请求指向在 CatsController 中定义的 create() 处理程序。如果在此过程中未调用拦截器的 handle() 方法,则 create() 方法不会被计算。只有 handle() 被调用(并且已返回值),最终方法才会被触发。为什么?因为Nest订阅了返回的流,并使用此流生成的值来为最终用户创建单个响应或多个响应。而且,handle() 返回一个 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`)), ); } }
提示
: NestInterceptor<T, R> 是一个通用接口,其中 T 表示 Observable<T> 的类型(支持响应流),R 是 Observable<R> 包裹的值的类型。
注意
: 拦截器,如控制器、提供者、Guard 等,可以通过它们的构造函数注入依赖项。
由于 handle() 返回一个 RxJS Observable,我们可以使用多种操作符来操作流。 在上面的示例中,我们使用了 tap()
运算符,它在可观察流的正常或异常终止时调用我们的匿名日志记录函数,但不会干扰响应周期。
绑定拦截器
为了设置拦截器,我们使用从 @nestjs/common 包导入的 @UseInterceptors() 装饰器。 像管道和 guard 一样,拦截器可以是控制器范围的、方法范围的或全局范围的。
cats.controller.ts
@UseInterceptors(LoggingInterceptor) export class CatsController {}
提示
: @UseInterceptors() 装饰器是从 @nestjs/common 包中导入的。
使用上述构造,CatsController 中定义的每个路由处理程序都将使用 LoggingInterceptor
。 当有人调用 GET /cats
端点时,我们将在标准输出中看到以下输出:
Before...
After... 1ms
请注意,我们传递了 LoggingInterceptor
类型(而不是实例),将实例化的责任留给了框架并启用了依赖注入。 与管道、guard 和异常过滤器一样,我们也可以传递一个就地实例:
@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 {}
Response 映射
我们已经知道 handle() 返回一个 Observable。 流包含从路由处理程序返回的值,因此我们可以使用 RxJS 的 map()
运算符轻松地对其进行变异。
警告
:响应映射功能不适用于特定于库的响应策略(禁止直接使用 @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 }))); } }
提示
: 嵌套拦截器可以使用同步和异步的 intercept() 方法。 如有必要,我们可以简单地将方法切换为异步。
通过上述构造,当有人调用 GET /cats
端点时,响应将如下所示(假设路由处理程序返回一个空数组 []):
{
"data": []
}
拦截器在为整个应用程序中出现的需求创建可重用的解决方案方面具有很大的价值。 例如,假设我们需要将每次出现的空值转换为空字符串 ''。 我们可以使用一行代码并全局绑定拦截器,以便每个注册的处理程序自动使用它。
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 ErrorsInterceptor implements NestInterceptor { intercept(context: ExecutionContext, next: CallHandler): Observable<any> { return next .handle() .pipe( catchError(err => throwError(() => new BadGatewayException())), ); } }
Stream 重写
有几个原因我们有时可能想要完全阻止调用处理程序并返回不同的值。 一个明显的例子是实现缓存以提高响应时间。 让我们看一个简单的缓存拦截器,它从缓存中返回响应。 在一个实际的例子中,我们想要考虑其他因素,如 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 并创建自定义装饰器。
更多操作符
使用 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
异常之前添加自定义逻辑(例如释放资源)。