在上一篇文章中分析了NestJS的过滤器的知识点之后,本文开始分析NestJS的拦截器的知识点。
本文是一套系列文章,有很强的前后联系,如果您对NestJS感兴趣的话,建议您从本系列的开头开始阅读。
先给大家打一些鸡血,拦截器跟过滤器的实现过程差不多,不会很难,结合着看会很快就能掌握的,哈哈哈,那我们就开始吧。
1、初探NestJS的运行原理之拦截器
是了解拦截器的原理之前,我们还是事先看一下在项目中该如何使用拦截器。
1.1、与中间件的对比
在我学习NestJS过程中,曾经存在一些误区,我是觉得看起来中间件上面暴露给我们了Request对象和Response对象,拦截器上虽然没有直接给我们暴露Request对象和Response对象,但是我们可以通过ExecutionContext获取到Request对象和Response对象,看起来好像中间件能做的拦截器也能做呀,后面深入学习之后,我发现我的理解是错误的。
以下是摘录自官网的NestJS对中间件能力的介绍:
Middleware functions can perform the following tasks:
- execute any code.
- make changes to the request and the response objects.
- end the request-response cycle.
- call the next middleware function in the stack.
- if the current middleware function does not end the request-response cycle, it must call
next()
to pass control to the next middleware function. Otherwise, the request will be left hanging.
然后是摘录在官网的NestJS对于拦截器能力的介绍:
Interceptors have a set of useful capabilities which are inspired by the Aspect Oriented Programming (AOP) technique. They make it possible to:
- bind extra logic before / after method execution
- transform the result returned from a function
- transform the exception thrown from a function
- extend the basic function behavior
- completely override a function depending on specific conditions (e.g., for caching purposes)
在此对它们做一个比较,中间件和拦截器都能在请求到达之前完成一些逻辑,中间件的执行时机要早于拦截器 ,正如NestJS官网所说,拦截器侧重点在AOP上,所以它的执行时机是在控制器的方法执行的前后调用的。中间件是可以中断不合法的请求的,而拦截器不可以。而拦截器是对控制器的方法的增强或削弱,它必须是要依附于控制器的方法。
另外,还有一个非常重要的差别,中间件无法知道处理当前请求的控制器和处理请求的控制器方法,因为拦截器依附于控制器的方法,所以它可以知道。因此,拦截器能够利用反射获取到控制器或方法上的一些元数据,从而实现类似鉴权这样的一些操作。
1.2 在NestJS中如何使用拦截器
以下是我在实际项目中使用拦截器对响应内容进行加密的例子:
ts
@Injectable()
export class EncryptInterceptor implements NestInterceptor {
constructor(private readonly reflector: Reflector) {}
public encrypt(txt: string) {
return CryptoJS.AES.encrypt(txt, encryptKey, { iv: encryptIv }).toString();
}
@AutoBind
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
const methodNoEncrypt = this.reflector.get<boolean>(
SKIP_ENCRYPT,
context.getHandler(),
);
const controllerNoEncrypt = this.reflector.get<boolean>(
SKIP_ENCRYPT,
context.getClass(),
);
return next.handle().pipe(
map((resp: unknown) => {
// 如果不是标准的返回,不进行任何处理,
// 如果有跳过方法或跳过控制器的标签则也不进行任何处理
// 非生产环境不加密
if (
Object.prototype.toString.call(resp) !== '[object Object]' ||
methodNoEncrypt ||
controllerNoEncrypt ||
!isProd
) {
return resp;
}
const response = resp as StandardResponse<any>;
const { data, ...rest } = response;
// 就算是字符串,也要stringify,前端再解密的时候没有任何心智负担,数据的规格始终是一致的
const jsonResult = JSON.stringify(data);
return {
...rest,
data: {
type: 'encrypt',
value: this.encrypt(jsonResult),
},
};
}),
);
}
}
挂载拦截器的操作跟之前我们聊的过滤器方式几乎是差不多的。
第一种方式,使用Provider的方式进行挂载,把拦截器交给IoC
容器管理,这种方式是全局的拦截器,同时还可以使用Scope
指定它的生命周期,到时候NestJS内部处理的逻辑是不一样的:
ts
import { APP_INTERCEPTOR } from '@nestjs/core';
@Module({
providers: [
{
provide: APP_INTERCEPTOR,
useClass: EncryptInterceptor,
},
],
})
export class AppModule {
}
第二种方式,使用NestApplication提供的方法useGlobalInterceptors
,这个时候,拦截器的初始化过程就是您自行初始化了,假设您的拦截器需要有依赖数据注入,那可能还需要额外处理一下,比如我的项目中的例子,用全局的API改写的话,就要写成这个样子:
ts
async function bootstrap() {
const app = await NestFactory.create(AppModule);
// 获取到IoC容器内部管理的某个类的实例
const reflectorIns = app.get(Reflector);
// 手动初始化拦截器,并且把拦截器需要的数据传递给它
app.useGlobalInterceptors(new EncryptInterceptor(reflectorIns));
await app.listen(config.PORT);
}
对于上述app.get这个方法不清楚的同学,可以查看一下官方文档的这个位置👉Standalone applications | NestJS - A progressive Node.js framework,将来我会再详细的聊聊IoC
容器那部分的细节,届时会对这个方法进行一些分析。
第三种方式就是针对某些小范围的业务逻辑使用拦截器。
ts
// 针对控制器进行拦截
@UseInterceptors(new LoggingInterceptor())
@Controller('/home')
export class CatsController {}
或者仅仅对某个方法进行拦截:
ts
@Controller('/home')
export class CatsController {
// 仅仅拦截某个方法
@Get('/demo')
@UseInterceptors(new LoggingInterceptor())
public async Demo() {
return "Hello world";
}
}
在方法上的拦截就真的我们在本系列文章的第一篇所讲的那种情况一模一样了,只不过拦截器跟一般装饰器(比如Get装饰器)的差异,一个体现在编译时,一个体现在运行时,拦截器就是运行时的AOP的手段,因此拦截器可能在某些情况下会非常有用。
关于拦截器的用法,我就给大家展示这些内容了,接下来我们就开始分析它的实现原理。
1.3 拦截器的运行原理分析
有了分析过滤器的经验,我们按图索骥就可以了。
首先看一下UseInterceptors这个装饰器到底做了什么。
ts
export function UseInterceptors(
...interceptors: (NestInterceptor | Function)[]
): MethodDecorator & ClassDecorator {
return (
target: any,
key?: string | symbol,
descriptor?: TypedPropertyDescriptor<any>,
) => {
// 省略
if (descriptor) {
// 省略
extendArrayMetadata(
INTERCEPTORS_METADATA,
interceptors,
descriptor.value,
);
return descriptor;
}
// 省略了一些判断逻辑
extendArrayMetadata(INTERCEPTORS_METADATA, interceptors, target);
return target;
};
}
因为它是一个复合装饰器,即既可以装饰类又可以装饰类的方法,所以上述的if判断就是在区别它的应用场景的。
然后,我们分析一下之前在介绍使用章节的时候提到的useGlobalInterceptors
API,看一下它是怎么样处理的。 还是跟过滤器一样,将其加入到ApplicationConfig上管理起来。 然后再看一下使用IoC
容器的方式管理的拦截器的方式: 绑定拦截器的过程基本上就这样了,接下来就是看一下拦截器是怎么样工作的。
我们应该把目光聚焦到哪个位置,大家应该很清楚了吧,哈哈哈,我们直接来看RouterExecutionContext这个类的create方法,在讲述路由那一章节我们就已经聊到过它了,只不过当时我们直接跨过了拦截器,因为重点不在拦截器嘛。
接下来看一下这个interceptorsContextCreator是干什么的,以及它是怎么来的。
它在NestJS解析路由和控制器方法的关系的时候初始化的。 在初始化的时候,把ApplicationConfig的实例传给了它,在之前我们已经知道了ApplicationConfig已经保存了一些全局的拦截器了。
InterceptorsContextCreator继承自ContextCreator,在过滤器那一节其实我们就已经看过了ContextCreator这个类,它是一个抽象类,它封装从控制器或者从方法获取一些元数据的能力。
在过滤器的时候,我们还看不出来它的精妙之处,不过在这个位置,已经有一种豁然开朗的感觉了,可以明确的看出这是一个经典的设计模式------模板方法模式
。 同样还是遵循之前我们在过滤器所知道的那个顺序,即全局拦截器->控制器拦截器->方法拦截器,至此,拦截器是怎么得到的流程我们已经明白了,接下来就看一下拦截器是在什么位置执行的即可。
拦截器是在这个位置执行的。 同样,interceptorsConsumer是在RouterExplorer那儿初始化的。 这个intercept方法大家如果看起来觉得晦涩难懂的话就不必太纠结,因为它是基于RxJS的。RxJS是一个学习路线非常陡峭,但是收益又比较低的库,我理解其大概意思这段代码就是一个Promise的链式调用,在上一个Promise状态变化以后再进行到下一个,直到完成所有的任务。 可以看到,在拦截器执行的时候,它是把执行上下文传递给我们了,然后链接上下一个拦截器,NestJS最终附加一个统一的处理。
这个位置我对RxJS的处理流程阐述的比较简单,如果同学有更好的表述,可以联系我修改。
最后,大家需要注意一下哦,拦截器的执行顺序是正序的,即全局拦截器->控制器拦截器->方法拦截器,仔细思考一下觉得这个流程也是有道理的,由宽泛到精确嘛,才能够对业务逻辑有一个更好的控制。
总结
拦截器、过滤器这几类工具API在实现的过程中用到了经典的设计模式,模板方法模式,对于模板方法模式的使用存在一些疑问的同学,可以参考我以前的博客👉模板方法模式。
- 拦截器的实现原理和过滤器是差不多的
- 拦截器的执行顺序跟过滤器是反的,拦截器的顺序是全局拦截器->控制器拦截器->方法拦截器。
- 拦截器也是装饰模式的体现,即在NestJS运行时完成插入逻辑。