深入了解Nest拦截器

2,893 阅读6分钟

拦截器

拦截器如同管道和守卫,都是以类的形态存在(也可以是一个function),并且注以@Injectable()将依赖关系给Nest托管。并且实现NestInterceptor接口。这个接口只有一个待实现的方法intercept(context,next),我们看一下源代码:

export interface NestInterceptor<T = any, R = any> {
  intercept(
    context: ExecutionContext,
    next: CallHandler<T>,
  ): Observable<R> | Promise<Observable<R>>;
}

每个拦截器被激活后,送入两个参数:当前的上下文和即将被执行的方法具柄(指向)。如果不太了解ExecutionContext,可以参考《守卫》部分的内容,两个完全是一个实例。next则是Observable异步接口,显然这是(指向)即将被执行的下个过程。如果你在这个拦截过程中不在使用next.handle(),那么这个请求过程就会至此停止,所以这个过程将包含执行前后的逻辑,也就是所谓(在请求和响应期间)的一个切面。

interceptor.png

面向切面编程

面向切面的编程(AOP)中带来的启发,拦截器有一组功能:

  • 在某个方法执行之前或者之后绑定额外的业务逻辑;例如参数的转换(字符串转换为DTO所需类型)或者结果转换(统一的API返回结果)等;
  • 扩展某个方法的基本功能;
  • 某些特定条件下阻断某个方法;

绑定拦截器

我们先创建一个转换拦截器Converter

nest g in Converter

将守卫中的部分代码复制过来,并稍作修改(注意:next变量会与接口的参数名称冲突):

import {
  CallHandler,
  ExecutionContext,
  Injectable,
  NestInterceptor,
} from '@nestjs/common';
import { NestFactory } from '@nestjs/core';
import { Observable } from 'rxjs';
import { map, tap } from 'rxjs/operators';
@Injectable()
export class ConverterInterceptor implements NestInterceptor {
  async intercept(
    context: ExecutionContext,
    next: CallHandler,
  ): Promise<Observable<any>> {
    const [request, response] = context.getArgs();
    switch (context.getType()) {
      case 'http':
        const [request, response, next_] = context.getArgs();
        console.log(
          `${request.protocol}://${request.hostname}${
            request.path
          }${JSON.stringify(request.query)} => ${context.getClass().name}.${
            context.getHandler().name
          }`,
        );
        break;
      case 'ws':
        break;
      case 'rpc':
        break;
    }
    return next.handle();
  }
}

看过《中间件》节的同学肯定会问,通过context.getArgs() 获得了一个next,是否可以通过这个来调用即将执行的业务?答案是不能,目前还没形成这个请求的业务执行栈,从上下文获取的next是一个空的栈,只能通过实用入口参数next来继续执行。

拦截器的绑定过程类似于异常过滤器和守卫的绑定方式,也是通过装饰器的方式对类或者方法,甚至注册在Module中成为全局的拦截器。我们照旧了解一下装饰器UseInterceptors的源代码:

export function UseInterceptors(
  ...interceptors: (NestInterceptor | Function)[]
): MethodDecorator & ClassDecorator {
  return (
    target: any,
    key?: string | symbol,
    descriptor?: TypedPropertyDescriptor<any>,
  ) => {
    const isInterceptorValid = <T extends Function | Record<string, any>>(
      interceptor: T,
    ) =>
      interceptor &&
      (isFunction(interceptor) ||
        isFunction((interceptor as Record<string, any>).intercept));

    if (descriptor) {
      validateEach(
        target.constructor,
        interceptors,
        isInterceptorValid,
        '@UseInterceptors',
        'interceptor',
      );
      extendArrayMetadata(
        INTERCEPTORS_METADATA,
        interceptors,
        descriptor.value,
      );
      return descriptor;
    }
    validateEach(
      target,
      interceptors,
      isInterceptorValid,
      '@UseInterceptors',
      'interceptor',
    );
    extendArrayMetadata(INTERCEPTORS_METADATA, interceptors, target);
    return target;
  };
}

仔细阅读过守卫装饰器的同学应该非常眼熟这段代码了,逻辑几乎完全一致。同样是:接收一个或者多个的拦截器,验证其有效性后作为类或者方法的元数据存储。它的类、方法和全局的绑定方法这里不在赘述,不太清楚的同学参考守卫或者异常过滤器章节。然后我们绑定到控制器等方法上:

import { Controller, Get, UseInterceptors } from '@nestjs/common';
import { AppService } from './app.service';
import { ConverterInterceptor } from './converter.interceptor';

@Controller()
export class AppController {
  constructor(private readonly appService: AppService) {}

  @Get()
  @UseInterceptors(ConverterInterceptor)
  getHello(): string {
    console.log('in controller.');
    return this.appService.getHello();
  }
}

当我们执行:

curl -X GET "http://localhost:3000/"
# http://localhost/{} => AppController.getHello

多个拦截器

多个拦截器可能是一个装饰器同时采用多个拦截器(同一层级),也有可能通过全局、类和方法分别绑定并发挥作用(不同层级)。我们可以通过一个实验来了解其过程,我们分别增加全局、类和方法三个拦截器,并在一个方法中加入2个拦截器,运行后观察其执行过程,代码类似如下:

@Injectable()
export class MethodInterceptor implements NestInterceptor {
  intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
    console.log('Method Interceptor');
    return next.handle().pipe(
      tap((data) => {
        console.log(`Result(Method):${data}`);
      }),
    );
  }
}

见其明显的入栈和出栈过程:

Global Interceptor
Class Interceptor
Method Interceptor
http://localhost/async{} => AppController.getHelloAsync
Result:Hello World!
Result(Method):Hi World!
Result(Class):Hi World!
Result(Global):Hi World!

拦截后续逻辑

前文重点都是在讲述拦截之前所做的工作,在调用next.handle()之后,就进入了下一个业务过程。如果需求要根据一些条件来修改返回值,那么就要针对返回结果来执行一些逻辑。回看一下代码,intercept方法的返回值类型是Observable,它是RxJS的核心类型,我们有必要对此作初步了解。

RxJS最简入门

限于篇幅,在这只是简单的介绍这个优秀的框架中与Nest有关部分,有兴趣的同学可以看官方文档。Rx = Reactive Extension,RxJS是一种响应式编程(RP)的实现框架。其中Observable字面意识是可观察,所谓的观察就是当事件发生时,由事件发生之前的所指派的函数来处理数据输入。这个指派的过程就是订阅,而事件的发生过程是一个发布。例如:股票的涨跌波动,加上一个时间轴后就形成了一个直角坐标系的波浪图。这一系列的数据,就形成了一个数据“流”。这些波动会引发一些操盘手的响应行动(如果没有波动情况,就不会引发交易),假如这个行动的逻辑是“追涨杀跌”,那么其仿真的代码如下:

import { fromEvent } from 'rxjs';
const VOLUME=10;
let observable$ = fromEvent(stockMaotai, 'fluctuate').subscribe((value) => {
	if (value > 0) {
    buyin(VOLUME);
  }else{
    sellout(VOLUME);
  }
});

如果整个过程只有一次产生数据,可以称之为单值流(single value stream)(大多数的Nest的请求就是单值)。就是响应式编程的核心概念。这个流中会三种类型的值,分别是正常产生的值,被触发的异常和整个流结束信号。产生值的过程可能是同步的也可能是异步的,Controller中的方法同样也是如此,借助RxJS,已经完美的将这两者统一,所以不论你后续(控制器中的方法或者下一个拦截器)同步或者是异步,操作方式都是一样的。

RxJS有两类操作符,一类是创建流的操作符,例如上面的fromEvent,还有常用的of等,另外一类是管道操作符,它就像在管道中添加什么化学物质,让流入管道的数据发生一定程度的改变,然后流出;

rxjs_pipe_interceptor.jpg

常用Pipe操作符

observable.pipe(op1,op2,op3...),pipe方法可以输入管道操作符。

  • map:可以修改输入的值,再输出。类似于JS的map函数;
  • tap:每次输入值后接收,但不改变值;
  • timeout:设置一个过期时间,当这个时间段内没有接收到完成信号就会出发这个逻辑;
  • catchError:当管道中发生异常(throw)后,执行的操作;

截获内容

在我们基本了解了RxJSstream的基本概念基础上,再来处理拦截器的后续内容,就比较容易理解了。我们输出执行业务后的结果:

@Injectable()
export class ClassInterceptor implements NestInterceptor {
  intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
    console.log('Class Interceptor');
    return next.handle().pipe(
      tap((data) => {
        console.log(`Result(Class):${data}`);
      }),
    );
  }
}

注意,next是指向下一个过程的对象,先通过handle()方法获得observable的对象,然后用pipe()方法将所需要操作逻辑加入。如果需要对结果就行一定对转换,可以使用map操作符:

    return next.handle().pipe(
      map((data) => {
        console.log(`Result:${data}`);
        return data.replace('Hello', 'Hi');
      }),
    );

跳过相应逻辑

如果这个业务设置了缓存,则通过条件判断后返回缓存内容(我个人建议针对缓存策略相关操作,更适合利用装饰器来承担相关业务逻辑)或者是别的什么方式就在当前拦截器中返回值,则不可以用直接返回Response的类型,由于还记得那个接口Observable<any>的限制吗?

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

@Injectable()
export class MethodInterceptor implements NestInterceptor {
  intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
    console.log('Method Interceptor');
    return of('Nothing');
  }
}

这里需要用到RxJS的创建流操作符of,从而创建一个新的stream

超时与异常捕获

这是一个非常实用的功能,当有一些请求异常(例如没有正确的返回结果或引发了时间较久的等待时,例如微信的回调要求5秒以内有响应,否则微信服务会认为服务器异常,而引发多次相同的请求),可以用timeout操作符。注意:由于超时属于异常情况,RxJS只是在规定的响应时间到后抛出一个异常,这个异常的处理还是需要开发者编写相关的逻辑。所以,在用timeout时,往往还需要搭配catchError(err)操作符:

import {
  CallHandler,
  ExecutionContext,
  Injectable,
  NestInterceptor,
} from '@nestjs/common';
import { Observable, of, throwError, TimeoutError } from 'rxjs';
import { catchError, tap, timeout } from 'rxjs/operators';

@Injectable()
export class GlobalInterceptor implements NestInterceptor {
  intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
    console.log('Global Interceptor');
    return next.handle().pipe(
      timeout(1000),
      catchError((err) => {
        if (err instanceof TimeoutError) {
          return of('Timeout Infomation');
        }
        return throwError(err);
      }),
      tap((data) => {
        console.log(`Result(Global):${data}`);
      }),
    );
  }
}

注意,catchError返回,同样也遵循借口的限制,一定要构造一个新的流返回。