在上一篇文章中分析了NestJS的路由处理流程之后,本文开始分析NestJS中间件知识点。
初探NestJS的运行原理之中间件
先看一下,我们在NestJS编写一个中间件的代码,以下是我在前文提到过的全局注入Request对象的中间件的例子。
ts
import { Injectable, NestMiddleware } from '@nestjs/common';
import { Request, Response, NextFunction } from 'express';
import { nanoid } from 'nanoid';
import { SingletonLoggerService } from '../services/logger/singleton-logger.service';
@Injectable()
export class TraceMiddleware implements NestMiddleware {
use(req: Request & { traceId: string }, res: Response, next: NextFunction) {
const uuid = nanoid(32);
req.traceId = uuid;
// 在中间件中注入request对象,可以使得每次打印的日志都有request上下文信息
SingletonLoggerService.getInstance().setRequest(req);
next();
}
}
然后是绑定中间件:
ts
// 省略了很多无关的代码
@Module({})
export class AppModule {
configure(consumer: MiddlewareConsumer) {
// 注册中间件
consumer.apply(TraceMiddleware).forRoutes('*');
}
}
在之前的文章,我们说过了,NestJS默认使用的是Express
提供的更底层的Http能力,于是我们首先看一下@nestjs/platform-express
这个包里面干了一些啥。
核心方法就是这个createMiddlewareFactory: 紧接着,我们还是利用跟踪堆栈的信息的办法来查看NestJS初始化我们编写的中间件的过程。 首先是我们调用NestJS提供的init方法,这个在之前的文章就已经提到过了(在NestJS启动过程中,我们调用init方法之前,对中间件管理的MiddlewareModule
模块就已经事先初始化了,因为它的定义在NestApplication
的构造器中) 在这之前,IoC
容器已经完成了模块和模块依赖的内容创建,我们已经可以通过模块拿到其对应的实例了。 此刻MiddlewareModule
就开始去读取中间件了。 在这个位置,我们回想一下,我们之前写在主模块里面的configure
方法,在此刻被调用了,中间件注册主线流程就完成了。 不过千万别捡了芝麻丢了西瓜,如果爱情有这么简单,那么就不会有那么多单身的人了,哈哈哈,我们需要详细探究一下NestJS的中间件是如何跟Express
绑定上去的,所以得看一下MiddlewareBuilder
做了什么事儿(还是像我们上文说的那个意思,现在中间件只通过了面试,甚至都没有拿到offer,它还没法工作)。
在MiddlewareBuilder
里面找了一圈,看起来并没有做什么事儿,是否遗漏了什么地方,我们尝试先往回找。 在NestJS的init方法里面,我们可能看到了它注册的链路,先顺着这个链路找一下,如果找不到的话到时候再回过来看看吧。 顺着这个调用链,可以追踪到MiddlewareModule
的registerMiddleware方法,看起来就是这儿了,运气还不错呀。 感觉快到和Express
的绑定逻辑了,我已经很迫不及待啦。 恭喜我们,已经到达终点了,在之前的文章我们已经知道了applicationRef就是我们注入进来的Express Adaptor
的实例,这个位置的逻辑就跟我们平时写Express
的use
函数一样的操作了。 我并不是刻意的避重就轻,我们需要重视这个registerHandler函数在之前的调用,(因为它体现了框架设计者的严谨,这也是我们学习源码的动力所在),所以现在回过头来分析它那一串很复杂的调用含义。 在代理里面绑定仅针对中间件的过滤器 上述流程,仅仅处理的是class类型的中间件,NestJS是在这个位置处理的函数式中间件:
其实跟class类型的中间件差不多,NestJS统一了函数式中间件,就是先要进行一遍转换,最终都转换成了class中间件再处理。
至此,我们大概已经完全明白了NestJS中间件的流程。
我们可以通过以上内容的学习得出某些结论:
- NestJS的中间件可以有很多个,并且并不是说我们一定要在主模块注册中间件。
- 如果模块仅针对自己的中间件,建议在每个模块内部编写中间件,而不要全部都写到主模块上,会让主模块很臃肿。
- 在写过滤器的时候,需要尽量谨慎的使用通配符(或者使用命名空间,比如/api/*)这种,因为通配符会应用到别的模块,可能这是你非预期的,避免造成潜在的问题。
除此之外,我们还能学到一点儿编程技巧,比如为什么它能这样链式调用呢:
ts
@Module({
imports: [],
controllers: [DemoController],
providers: [],
})
export class DemoModule {
configure(consumer: MiddlewareConsumer) {
consumer
.apply(DemoMiddleware)
.forRoutes('/demo')
.apply((req, res, next) => {
console.log('hello world');
debugger;
next();
})
.forRoutes('*');
}
}
它的源码的关键部分如下:
ts
export class MiddlewareBuilder implements MiddlewareConsumer {
public apply(
...middleware: Array<Type<any> | Function | any>
): MiddlewareConfigProxy {
return new MiddlewareBuilder.ConfigProxy(
this,
flatten(middleware),
this.routeInfoPathExtractor,
);
}
// 省略了一些无关的代码
private static readonly ConfigProxy = class implements MiddlewareConfigProxy {
constructor(
private readonly builder: MiddlewareBuilder,
private readonly middleware: Array<Type<any> | Function | any>,
private routeInfoPathExtractor: RouteInfoPathExtractor,
) {
// 这个位置的写法就相当于 this.middleware = middleware
}
public forRoutes(
...routes: Array<string | Type<any> | RouteInfo>
): MiddlewareConsumer {
// 进行了某些操作
return this.builder;
}
};
}
middleware这个变量,这个位置有点儿像哨兵(sentinel
)的味道了,如果你钻研过某些开源库的话,这种编程手段在Vue2源码中实现双向绑定也是运用了这种方式。每当我们调用apply方法之后,哨兵数组上就绑定好了我们添加的中间件,当我们调用foRoutes方法的时候,消费掉哨兵数组上的中间件,下次调用的时候,哨兵数组又被赋值为下一次的中间件列表,从而可以实现优雅的链式调用。
在下一节内容,我们将开始分析NestJS的过滤器,拦截器,守卫等内容的技术细节,敬请期待!