likes
comments
collection
share

记录我的NestJS探究历程(五)——中间件

作者站长头像
站长
· 阅读数 3

在上一篇文章中分析了NestJS的路由处理流程之后,本文开始分析NestJS中间件知识点。

初探NestJS的运行原理之中间件

在NestJS中如何编写中间件

先看一下,我们在NestJS编写一个中间件的代码,以下是我在前文提到过的全局注入Request对象的中间件的例子。

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();
  }
}

然后是绑定中间件:

// 省略了很多无关的代码
@Module({})
export class AppModule {
  configure(consumer: MiddlewareConsumer) {
    // 注册中间件
    consumer.apply(TraceMiddleware).forRoutes('*');
  }
}

NestJS的中间件解析过程

在之前的文章,我们说过了,NestJS默认使用的是Express提供的更底层的Http能力,于是我们首先看一下@nestjs/platform-express这个包里面干了一些啥。

核心方法就是这个createMiddlewareFactory: 记录我的NestJS探究历程(五)——中间件 记录我的NestJS探究历程(五)——中间件 紧接着,我们还是利用跟踪堆栈的信息的办法来查看NestJS初始化我们编写的中间件的过程。 记录我的NestJS探究历程(五)——中间件 首先是我们调用NestJS提供的init方法,这个在之前的文章就已经提到过了(在NestJS启动过程中,我们调用init方法之前,对中间件管理的MiddlewareModule模块就已经事先初始化了,因为它的定义在NestApplication的构造器中) 记录我的NestJS探究历程(五)——中间件 记录我的NestJS探究历程(五)——中间件 在这之前,IoC容器已经完成了模块和模块依赖的内容创建,我们已经可以通过模块拿到其对应的实例了。 记录我的NestJS探究历程(五)——中间件 此刻MiddlewareModule就开始去读取中间件了。 记录我的NestJS探究历程(五)——中间件 在这个位置,我们回想一下,我们之前写在主模块里面的configure方法,在此刻被调用了,中间件注册主线流程就完成了。 记录我的NestJS探究历程(五)——中间件 不过千万别捡了芝麻丢了西瓜,如果爱情有这么简单,那么就不会有那么多单身的人了,哈哈哈,我们需要详细探究一下NestJS的中间件是如何跟Express绑定上去的,所以得看一下MiddlewareBuilder做了什么事儿(还是像我们上文说的那个意思,现在中间件只通过了面试,甚至都没有拿到offer,它还没法工作)。

我们注册的中间件是如何生效的

MiddlewareBuilder里面找了一圈,看起来并没有做什么事儿,是否遗漏了什么地方,我们尝试先往回找。 记录我的NestJS探究历程(五)——中间件 在NestJS的init方法里面,我们可能看到了它注册的链路,先顺着这个链路找一下,如果找不到的话到时候再回过来看看吧。 记录我的NestJS探究历程(五)——中间件 顺着这个调用链,可以追踪到MiddlewareModule的registerMiddleware方法,看起来就是这儿了,运气还不错呀。 记录我的NestJS探究历程(五)——中间件 感觉快到和Express的绑定逻辑了,我已经很迫不及待啦。 记录我的NestJS探究历程(五)——中间件 恭喜我们,已经到达终点了,在之前的文章我们已经知道了applicationRef就是我们注入进来的Express Adaptor的实例,这个位置的逻辑就跟我们平时写Expressuse函数一样的操作了。 记录我的NestJS探究历程(五)——中间件 我并不是刻意的避重就轻,我们需要重视这个registerHandler函数在之前的调用,(因为它体现了框架设计者的严谨,这也是我们学习源码的动力所在),所以现在回过头来分析它那一串很复杂的调用含义。 记录我的NestJS探究历程(五)——中间件 在代理里面绑定过滤器 记录我的NestJS探究历程(五)——中间件 对于中间件的过滤器处理,跟后文要向大家阐述的拦截器、守卫、管道的过滤器有些许处理流程上的差异,不过都是使用的是既有过滤器,即没有特定的处理逻辑,一个全局过滤器可以处理所有控件上抛出的错误

函数式中间件与类中间件的处理差异

上述流程,仅仅处理的是class类型的中间件,NestJS是在这个位置处理的函数式中间件:

记录我的NestJS探究历程(五)——中间件 记录我的NestJS探究历程(五)——中间件 其实跟class类型的中间件差不多,NestJS统一了函数式中间件,就是先要进行一遍转换,最终都转换成了class中间件再处理。

至此,我们大概已经完全明白了NestJS中间件的流程。

结论

我们可以通过以上内容的学习得出某些结论:

  • NestJS的中间件可以有很多个,并且并不是说我们一定要在主模块注册中间件。
  • 如果模块仅针对自己的中间件,建议在每个模块内部编写中间件,而不要全部都写到主模块上,会让主模块很臃肿。
  • 在写过滤器的时候,需要尽量谨慎的使用通配符(或者使用命名空间,比如/api/*)这种,因为通配符会应用到别的模块,可能这是你非预期的,避免造成潜在的问题。

除此之外,我们还能学到一点儿编程技巧,比如为什么它能这样链式调用呢:

@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('*');
  }
}

它的源码的关键部分如下:

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的过滤器,拦截器,守卫等内容的技术细节,敬请期待!