拦截器 Interceptors

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

img

拦截器具有一组有用的功能,这些功能受到 面向方面编程 (AOP) 技术的启发。它们可以实现以下功能:

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

基础知识

每个拦截器都实现 intercept() 方法,该方法接受两个参数。第一个是 ExecutionContext 实例(与 guards 完全相同的对象)。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

ts
TS
ts
// logging.interceptor
import { CallHandler, ExecutionContext, Injectable, NestInterceptor } 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> 的类型(支持响应流),而 RObservable<R> 包装的值的类型。

通知

拦截器(如控制器、提供程序、保护程序等)可以通过其 构造函数 注入依赖项

由于 handle() 返回 RxJS Observable,因此我们可以使用多种运算符来操作流。在上面的示例中,我们使用了 tap() 运算符,它在可观察流正常或异常终止时调用我们的匿名日志记录函数,但不会干扰响应周期。

绑定拦截器

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

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

@UseInterceptors() 装饰器从 @nestjs/common 包导入。

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

Before...
After... 1ms

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

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

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

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

ts
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 {}
提示

使用此方法对拦截器执行依赖注入时,请注意,无论使用此构造的模块是什么,拦截器实际上都是全局的。应该在哪里执行此操作?选择定义拦截器(上例中的LoggingInterceptor)的模块。此外,useClass不是处理自定义提供程序注册的唯一方法。了解更多信息此处(/fundamentals/custom-providers)。

响应映射

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

警告

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

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

ts
TS
ts
// transform.interceptor
import { CallHandler, ExecutionContext, Injectable, NestInterceptor } 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() 方法配合使用。如有必要,您可以简单地将方法切换为 async

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

json
{
  "data": []
}

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

ts
TS
ts
import { CallHandler, ExecutionContext, Injectable, NestInterceptor } 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() 运算符来覆盖抛出的异常:

TS
ts
// errors.interceptor
import {
  BadGatewayException,
  CallHandler,
  ExecutionContext,
  Injectable,
  NestInterceptor,
} 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())),
      )
  }
}
JS
js
// errors.interceptor
import { Injectable, BadGatewayException } from '@nestjs/common';
import { throwError } from 'rxjs';
import { catchError } from 'rxjs/operators';

@Injectable()
export class ErrorsInterceptor {
  intercept(context, next) {
    return next
      .handle()
      .pipe(
        catchError(err => throwError(() => new BadGatewayException())),
      );
  }
}

流覆盖

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

ts
TS
ts
// cache.interceptor
import { CallHandler, ExecutionContext, Injectable, NestInterceptor } 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 并创建自定义装饰器。Reflectorguards 一章中有很好的描述。

更多运算符

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

ts
TS
ts
// timeout.interceptor
import { CallHandler, ExecutionContext, Injectable, NestInterceptor, RequestTimeoutException } from '@nestjs/common'
import { Observable, TimeoutError, throwError } 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 之前添加自定义逻辑(例如释放资源)。