Nest —— Interceptors

2,327 阅读10分钟

前言

       之前匆匆写了一篇关于Nest官文的翻译,然而写的时候比较着急,所以很多地方的翻译比较马虎,甚至直接丢进翻译器在丢出来....(有的时候我会被丢进翻译器之后被丢出来的译文吓到,原因是...译文比自己翻译的好/(ㄒoㄒ)/~~)但还是有很多部分是翻译器解决不了的,语句通顺固然优雅锦上添花,但一点小的错误却是致命的。
       现在手头比较悠闲,也打算重新修改一份比较优雅的中文文档。有人会问花这么多时间写这个东西,是不是真的有用。百度上面也有一些关于Nest的文档,完全也不会有人来看你写的翻译。我的感受是,可能这就是我的学习方式吧。其实平时阅读文档,大多数情况下都是脑子说会了,手说不会。一边翻译英文文档,一遍理解框架的含义,还有助于提高阅读英文文档的能力。手敲过一遍和眼睛看过一遍真的不太一样,而且这种方式会增加使用时的自信。之后打算将每一章节分开书写,最后再通过链接汇总到一篇Nest妲己大记中去。就算没有人看,自己想要翻阅文档的时候,也可以拿出来看看,还能改改。这是一种乐趣,就好像养成游戏一样。



正文 Interceptors

interceptor拦截器是一个注释了装饰器 @Injectable 的类。拦截器需要实现 NestInterceptor 接口。

拦截器有一套有用的功能,受到AOP编程范式的启发。他们可以:

  • 在方法之前/之后绑定额外的逻辑。
  • 转换函数返回的结果
  • 转换函数抛出的异常
  • 扩展基础函数的行为
  • 根据特定的情况完全重载函数



Basic 基础

每个拦截器都实现了 interceptor() 方法,该方法接受两个参数。 第一个是 ExecutionContext 实例(与守卫中的对象完全相同)。 ExecutionContext 继承自 ArgumentsHost 。我们在前面的章节中介绍过他,他是一个传入原生处理程序的对参数的包装,并且基于你的应用类型包含了不同的参数数组。



Execution context 执行上下文

通过扩展 ArgumentsHostExecutionContext 也增加了一下新的辅助函数,为当前执行处理提供了一下额外的信息。这些信息在构建通用的拦截器时非常有用,他们可以跨控制器、方法、执行上下文而工作。



Call handler 参数CallHandler

第二个参数是一个 CallHandlerCallHandler 接口实现了 handle() 方法,你能够在拦截器的某些位置调用路由助理程序。如果你不在 interceptor() 方法的实现中调用 handle() 方法,路由处理程序就不会执行。

这样的方式意味着 interceptor() 方法实际上包裹了请求响应流。如此一来,你既可以在路由处理程序前和后实现自定义的逻辑处理。非常清楚的是,你可以在 interceptor() 方法中调用 handle() 之前书写你的代码,但是你如何影响后来发生的事情?因为 handle() 方法返回了一个Observable流,我们可以使用强大的RxJS操作符来操作响应结果。使用AOP技术,路由处理程序的调用叫做一个Pointcut,表示我们插入的额外的逻辑所在的点。

思考例如一个 POST /cats 请求。该请求的目标是定义在 CatsController 中的 create() 处理程序。如果一个拦截器没有调用 handle() 方法, create() 方法将不会被执行。一旦 handle() 被调用之后(并且Obeservable数据流被返回), create() 处理器就会被触发。另外一旦响应的数据流通过Obeservable接收到,就可以在数据流上进行额外的操作,返回一个最终的结果个调用者。



Aspect interception 拦截器外貌

第一个用例我们关注,使用拦截器记录用户的交互。

// 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}`))
      )
  }
}

Hints
NestInterceptor<T, R>是一个泛型接口,T表示 Observable 的类型,而R是由 Observable 包裹的值的类型。

NOTICE
拦截器,就像是控制器,providers,守卫等等,都可以通过constructor注入依赖。

由于 handle() 返回的是一个RxJS的Observable流,我们有非常多的额操作符的选择使用来操作数据流。在上面的例子中,我们使用了 tap() 操作符,在observable数据流优雅或异常的终止时调用我们的匿名日志函数,但是不会干扰响应周期。



Binding interceptors 绑定拦截器

要设置拦截器,我们使用 @UseInterceptors() 装饰器。就像管道和守卫,拦截器可以使控制器返回、方法范围、或者全局范围的。

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

Hints
import { UseInterceptors } from '@nestjs/common'

使用上面的建设,每个路由定义在 CatsController 里的处理程序都将使用 LoggingInterceptor 拦截器。当有人调用 GET /cats 节点,你会看到下面的输出

Before...
After... 1ms

注意我们传入的 LoggingInterceptor 类型(不是实例),将实例化的职责交给框架,并启用依赖注入。就像管道,守卫,异常过滤器,我们也可以就地传入一个实例。

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

正如所提到的,上面的建设为所有声明的处理程序添加了拦截器。如果我们想要将拦截器的返回限制在一个方法,我们只要将装饰器应用到方法层级就可以了。

要设置一个全局拦截器,我们在Nest应用实例上使用 useGlobalInterceptors() 方法:

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

全局的拦截器在整个应用,所有的控制器,路由处理程序中被使用。按照依赖注入,任何在模块以外注册的全局兰凝结器都不可以注入依赖,因为这一步是在模块的上下文之外完成的。为了解决这个问题,你可以通过一下方法来设置拦截器:

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

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

Hints
当使用这种方式来为拦截器实现依赖注入的时候,请注意无论这种结构使用在哪个模块,拦截器实际上都是全局的。选择拦截器所定义的的模块即可。另外useClass也不是处理自定义provider注册的唯一方法。



Response mapping 响应映射

我们已经知道 handle() 方法返回一个Observable流。这个流包含了从路由处理程序返回的值,然后我们可以简单地使用RxJS的 tap() 操作符来操作他。

WARNING
响应映射的特性在使用library-specific响应策略的时候不会工作。

让我们创建一个 TransformInterceptor 拦截器,来修改每个响应。这里会使用 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 })))
  }
}

Hints
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))
  }
}



Exception mapping 异常映射

另一个有趣的用例就是利用RxJs的 catchError() 的操作符来重载抛出的异常。

//error.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<ant> {
    return next
      .handle()
      .pipe(
        catchError(err => throwError(new BadGatewayException()))
      )
  }
}



Stream overriding 流重载

我们有的时候可能想要完全阻止调用处理程序,而是返回一个不同的值。一个明显的例子是实现一个缓存来提高响应的时间。让我们来看看这个简单的缓存拦截器,它从缓存中返回响应的值。在现实子中,我们可能还需要考虑其他的因素例如TTL、缓存验证、缓存大小等等。但是超出了目前的讨论范围。这里有一个简单的例子:

//cache.interceptor.ts
import { Injectable, NestInterceptor, ExecutionContext, CallHandler } form 'rxjs'
import { Observable, of } from 'rxjs'

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

我们拦截器有一个isCached变量和一个响应[]。要注意的重点是我们这里返回了一个由RxJS的 of() 操作符创建的新的流。如此路由处理程序就不会被调用。当有人访问这个使用拦截器的节点时,响应就会立刻返回。要创建一个更加通用的解决方案,你可以利用Reflector并创建自定义装饰器。



More operators 更多操作符

使用RxJS的操作符给我们许多能力。让我们思考另一个普遍的用例。想象你要处理超时的请求。当你的节点在一段时间后没有返回任何东西,你想要终止并返回一个错误响应。看下面的例子:

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

@Injectable()
export class TimeoutInterceptor implements NestInterceptor {
  intercept(context: ExecutionContext, next: CallHandle): Observable<any> {
    return next.handle().pipe(timeout(5000))
  }
}

5秒之后,请求进程将会被取消。





后记

原文地址: docs.nestjs.com/interceptor…


关于本文

  • 文章非复制黏贴,经浏览文档,以自己的理解进行,代码测试,手打书写。本篇为翻译+意译。
  • 用作记录自己曾经学习、思考过的问题的一种笔记。
  • 用作前端技术交流分享。
  • 阅读本文时欢迎随时质疑本文的准确性,将错误的地方告诉我。本人会积极修改,避免文章对读者的误导。

关于我

  • 是一只有梦想的肥柴。
  • 觉得算法、数据结构、函数式编程、js底层原理等十分有趣的小前端。
  • 志同道合的朋友请关注我,一起交流技术,在前端之路上共同成长。
  • 如对本人有任何意见建议尽管告诉我哦~ 初为肥柴,请多多关照~
  • 前端路漫漫,技术学不完。今天也是美(diao)好(fa)的一天( 跪了...orz