拦截器
拦截器如同管道和守卫,都是以类的形态存在(也可以是一个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(),那么这个请求过程就会至此停止,所以这个过程将包含执行前后的逻辑,也就是所谓(在请求和响应期间)的一个切面。
面向切面编程
从面向切面的编程(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等,另外一类是管道操作符,它就像在管道中添加什么化学物质,让流入管道的数据发生一定程度的改变,然后流出;
常用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返回,同样也遵循借口的限制,一定要构造一个新的流返回。