记录我的NestJS探究历程(十)——编写插件

前言

从这篇文章开始,我将不会再按一些既定的顺序给大家阐述自己的一些经验了,未来的文章将会是比较自由的,想到什么就写什么,哈哈哈。

您如果认真学习完成上一篇文章之后,基本上熟练应用NestJS开发项目已经是没有任何问题的了,遇到问题也能够快速的定位,但是我们肯定不满足已有的知识点,于是让我们来进一步的学习吧。

本文跟前文阐述的路由,中间件,拦截器,守卫、管道没有什么联系,如果你没有看过我之前的这些文章也没有关系,但是本文需要您已经知道一些NestJS基本的IoC管理的一些知识点,对于这部分知识点,您可以查阅本系列文章的第3篇关于IoC容器一些基础的原理分析👉记录我的NestJS探究历程(三)------依赖注入起步

本文暂时没有之前那么多晦涩难懂的代码阐述,哈哈哈。主要是通过向大家阐述动态模块和自定义Provider的知识点,结合着@nestjs/axios的源码,让大家知道如何编写NestJS的插件,最后向大家展示了一个我自己编写的一个插件的例子。

自定义Provider

您在实际的项目中应该都是这样注入IoC容器管理的Provider吧?

ts 复制代码
@Module({
  imports: [
    HttpModule,
  ],
  providers: [
    PageService
  ],
})
export class FoundationCoreModule {}

实际上,上面的写法其实等价于:

ts 复制代码
@Module({
  imports: [
    HttpModule,
  ],
  providers: [
    provide: PageService,
    useClass: PageService
  ],
})
export class FoundationCoreModule {}

但实际上Provider的写法还有很多种的,以下就列举NestJS官网上你可能没有见过的Provider的写法及实际开发中的用途。

使用token作为provide的依据

ts 复制代码
const MODULE_TOKEN = 'module-key';
// 模拟一些配置
const someConfig = {};

@Module({
  imports: [
    HttpModule,
  ],
  providers: [
    // 以MODULE_TOKEN这个元数据的Key作为IoC容器管理的依据
    provide: MODULE_TOKEN,
    useClass: PageService
  ],
})
export class FoundationCoreModule {}

正常情况下,我们用编写的Service作为IoC容器管理的Key就足够,但是在某些时候不行,就比如我们在之前的文章就看过我给出的例子,我们想遵循面向接口编程的规范,那么provide选择就不能指定那个接口实现类作为IoC容器管理的Key(有点儿绕口令的感觉,但我确定没有说错),必须使用以下的这种方式:

ts 复制代码
import { RiskCtrlHttpService } from './risk-ctrl.http.service';
import { RISK_CTRL_TOKEN } from './risk-ctrl.constants';

@Module({
  imports: [HttpModule],
  providers: [
    {
      // 用一个特征字符串表示
      provide: RISK_CTRL_TOKEN,
      useClass: RiskCtrlHttpService,
    },
  ],
})
export class AppModule {}

在使用这个Service的时候,仅仅只需要做简单的调整就可以,比如:

ts 复制代码
@Injectable()
export class KtvService {
  // 用字符串作为注入的依据
  @Inject(RISK_CTRL_TOKEN)
  protected readonly riskCtrlService: IRiskCtrlService;
}

或者:

ts 复制代码
@Injectable()
export class KtvService {
  constructor(
    @Inject(RISK_CTRL_TOKEN)
    protected readonly riskCtrlService: IRiskCtrlService,
  ) {}
}

NestJS的官网有明确的说明,对于这类TOKEN变量,最好使用一个文件来进行统一管理

useValue

ts 复制代码
// 模拟一些配置
const someConfig = {};

@Module({
  imports: [
    HttpModule,
  ],
  providers: [
    // 以PageService这个元数据的Key作为IoC容器管理的依据
    provide: PageService,
    useValue: someConfig
  ],
})
export class FoundationCoreModule {}

这种用法跟我们平常的写法没有任何区别,唯一的不同就是NestJS知道我们托管给它的是一个对象字面量,它不需要再去创建了。

对于这种写法,我就不给大家展示在使用了。

useExisting

这个用法就是创建一个别名而已,在实际的开发中几乎用不到。在NestJS的源码中能看到一处这样的用法。

ts 复制代码
const ReflectorAliasProvider = {
  provide: Reflector.name,
  useExisting: Reflector,
};
// 已省略部分非关键代码
@Module({
  providers: [
    ReflectorAliasProvider,
  ],
})
export class InternalCoreModule {}

useFactory

这个是重点,也是难点,这个在实际的开发中主要是为了开发插件,使用十分广泛。

看一下关于useFactory部分的类型定义。

ts 复制代码
export interface FactoryProvider<T = any> {
  // 被IoC容器管理的Key
  provide: InjectionToken;
  // 使用工厂函数创建被IoC容器管理的内容
  useFactory: (...args: any[]) => T | Promise<T>;
  // 在使用工厂函数创建内容时,所依赖的其它的选项,这些选项必须要提前被IoC容器管理
  inject?: Array<InjectionToken | OptionalFactoryDependency>;
  scope?: Scope;
  durable?: boolean;
}

export type OptionalFactoryDependency = {
  token: InjectionToken;
  optional: boolean;
};

在NestJS的官方文档中,还有一句非常重要的话,大家看一下。

The syntax for this is to use async/await with the useFactory syntax. The factory returns a Promise, and the factory function can await asynchronous tasks. Nest will await resolution of the promise before instantiating any class that depends on (injects) such a provider.

以上内容,我个人的理解就是说,NestJS的IoC容器在工作的时候,必须要等待useFactory上的异步任务fulfilled,才能创建这个Provider的实例

在目前,我还没有使用过useFactory,不过我倒是知道它怎么用,一会儿就举一个实际的例子为大家分析一下,哈哈。

动态模块

关于动态模块,NestJS的官网阐述的相当复杂,但是我觉得看懂了它的描述倒也是可以换一种简单的方式来描述了。

动态模块并不是指在运行时加载模块(这个技术叫懒加载,后续我们再聊它),所谓的动态模块,主要就是为了解决静态模块没有可编程性的问题,在项目中肯定不是所有的东西都是事先能够确定的,它解决了而是某些Provider的创建有先决条件的问题。

比如,我的一些Service需要依赖一些配置,但是这些配置又来源于IoC容器,这肯定是要程序启动起来的时候才知道的,对于这类的Service如果单单只用静态模块的话,就感觉有点巧妇难为无米之炊的窘迫了,所以才有了动态模块这种技术。

有了动态模块,整个NestJS的模块设计就完全升华了,因为这就意味着模块能够很容易的被抽离进行单独管理,这就为第三方开发者丰富NestJS的插件生态提供了可能。

动态模块,是一个class,必须提供某些静态方法供注册的时候调用,但是希望大家遵守一些规范,以下是摘录自官网的一些要求。

  • register, you are expecting to configure a dynamic module with a specific configuration for use only by the calling module. For example, with Nest's @nestjs/axios: HttpModule.register({ baseUrl: 'someUrl' }). If, in another module you use HttpModule.register({ baseUrl: 'somewhere else' }), it will have the different configuration. You can do this for as many modules as you want.
  • forRoot, you are expecting to configure a dynamic module once and reuse that configuration in multiple places (though possibly unknowingly as it's abstracted away). This is why you have one GraphQLModule.forRoot(), one TypeOrmModule.forRoot(), etc.
  • forFeature, you are expecting to use the configuration of a dynamic module's forRoot but need to modify some configuration specific to the calling module's needs (i.e. which repository this module should have access to, or the context that a logger should use.)

All of these, usually, have their async counterparts as well, registerAsync, forRootAsync, and forFeatureAsync, that mean the same thing, but use Nest's Dependency Injection for the configuration as well.

然后,您定义的这些方法必须返回一个类型,这个类型叫做DynamicModule,它只有一个module参数是必须的。

ts 复制代码
export interface ModuleMetadata {
  imports?: Array<
    Type<any> | DynamicModule | Promise<DynamicModule> | ForwardReference
  >;
  controllers?: Type<any>[];
  providers?: Provider[];
  exports?: Array<
    | DynamicModule
    | Promise<DynamicModule>
    | string
    | symbol
    | Provider
    | ForwardReference
    | Abstract<any>
    | Function
  >;
}

export interface DynamicModule extends ModuleMetadata {
  // 这个参数是必须的
  module: Type<any>;
  global?: boolean;
}

其实也就是我们用编程的方式返回我们在静态模块在@Module装饰器里面写的那些东西。

我知道读者您看到这儿肯定是一头雾水的,听君一席话如听君一席话,先别着急骂我,我们接下来看一个实际的例子您就明白了。

@nestjs/axios的源码分析

为什么向大家阐述@nestjs/axios呢,因为它真的足够简单,但是它又把之前我们聊到的自定义Provider里面的核心知识和动态模块的关键体现的淋漓尽致,这种低投入高收入简直太爽了。

这里是它的仓库地址👉@nestjs/axios

首先是HttpService的实现,这个实现需要外接注入一个axios的实例,一会儿我们看看这个实例是怎么来的。 接着就是HttpModule的实现,我们把它拆成几块来分析,你会觉得真的超级简单。

第一块,就是我们直接静态调用的场景。

ts 复制代码
@Module({
  providers: [
    HttpService,
    {
      provide: AXIOS_INSTANCE_TOKEN,
      useValue: Axios,
    },
  ],
  exports: [HttpService],
})
export class HttpModule {}

之前HttpService依赖外部注入一个Axios的实例,此刻Module的Provider列表已经提供了这个实例,所以是能够成注入的。这种方法就跟我们在写前端项目这样的操作是一样的。

js 复制代码
<template>
    <button @click="handleClick">点击请求</button>
</template>

<script>
import axios from 'axios'
export default {
    methods: {
        handleClick() {
            axios.get('https://www.baidu.com')
        }
    }
}
</script>

然后是同步方式的调用,这个时候就已经运用到了动态模块的知识点了,因为这个时候需要我们传入一些Axios的配置。

我们删减代码,得到第二块的调用场景代码。

ts 复制代码
export class HttpModule {
  static register(config: HttpModuleOptions): DynamicModule {
    return {
      // 动态模块必须要返回一个module
      module: HttpModule,
      providers: [
        {
          provide: AXIOS_INSTANCE_TOKEN,
          useValue: Axios.create(config),
        },
        /*{
          provide: HTTP_MODULE_ID,
          useValue: randomStringGenerator(),
        },*/
      ],
    };
  }
}

这个HTTP_MODULE_ID没有看到引用,不知道什么干什么的,所以大家也可以不用管。 还是回到之前我们提到的,因为HttpService里面需要一个Provider,然后在动态模块使用用户传入的配置生成,并委托给IoC容器管理,HttpService里面就可以正常注入了。

对于NestJS给出的使用文档,即如下:

ts 复制代码
@Module({
  imports: [
    HttpModule.register({
      timeout: 5000,
      maxRedirects: 5,
    }),
  ],
  providers: [CatsService],
})
export class CatsModule {}

第三块,也就是要用到我们之前说的useFactory了,之前没有向大家介绍使用办法,主要就是在这个位置会有demo,这样可以节省文章的篇幅,哈哈哈。

ts 复制代码
export class HttpModule {
  static registerAsync(options: HttpModuleAsyncOptions): DynamicModule {
    return {
      module: HttpModule,
      imports: options.imports,
      providers: [
        ...this.createAsyncProviders(options),
        {
          provide: AXIOS_INSTANCE_TOKEN,
          useFactory: (config: HttpModuleOptions) => Axios.create(config),
          inject: [HTTP_MODULE_OPTIONS],
        },
        /*{
          provide: HTTP_MODULE_ID,
          useValue: randomStringGenerator(),
        },*/
        ...(options.extraProviders || []),
      ],
    };
  }

  private static createAsyncProviders(
    options: HttpModuleAsyncOptions,
  ): Provider[] {
    if (options.useExisting || options.useFactory) {
      return [this.createAsyncOptionsProvider(options)];
    }
    return [
      this.createAsyncOptionsProvider(options),
      {
        provide: options.useClass,
        useClass: options.useClass,
      },
    ];
  }

  private static createAsyncOptionsProvider(
    options: HttpModuleAsyncOptions,
  ): Provider {
    if (options.useFactory) {
      return {
        provide: HTTP_MODULE_OPTIONS,
        useFactory: options.useFactory,
        inject: options.inject || [],
      };
    }
    return {
      provide: HTTP_MODULE_OPTIONS,
      useFactory: async (optionsFactory: HttpModuleOptionsFactory) =>
        optionsFactory.createHttpOptions(),
      inject: [options.useExisting || options.useClass],
    };
  }
}

这是对于创建Axios的实例时,需要依赖异步任务的完成的调用方式了。

我们先看一下NestJS官方给我们的例子。

ts 复制代码
HttpModule.registerAsync({
  imports: [ConfigModule],
  useFactory: async (configService: ConfigService) => ({
    timeout: configService.get('HTTP_TIMEOUT'),
    maxRedirects: configService.get('HTTP_MAX_REDIRECTS'),
  }),
  inject: [ConfigService],
});

我们回过头来看一下源码。

首先NestJS是提供了一个AXIOS_INSTANCE_TOKEN的注入,这个是用来托管Axios的实例的,即HttpService里面需要的那个实例,这个位置我们是没有什么疑问的。

关键就是在委托AXIOS_INSTANCE_TOKENIoC容器的时候,多了一个inject,它的Key是HTTP_MODULE_OPTIONS,这个Key就是我们要传递给Axios实例的配置对象,但是得到这个对象的过程是异步的。

顺着这逻辑,我们就可以看到调用了createAsyncProviders方法,并且用户使用了useFactory选项的话,就再次调用了createAsyncOptionsProvider这个方法,在这个方法里面,透传了外界指定的inject选项,并且把外界的useFactory也透传给HTTP_MODULE_OPTIONS作为IoC容器创建这个配置的依据。

至此,大家就明白了官方给我们的例子了吧,我们使用异步任务,在其成功之后得到一个Axios的配置并委托给IoC容器,然后用这个配置创建Axios的实例,最后把这个实例注入给HttpService。

现在大家再回过头看,是不是就像我说的@nestjs/axios非常具有代表性,它提供了3种调用方式给用户选择,实现这3个调用方式的过程用到了自定义的Provider和动态模块的知识点。

编写NestJS的插件

在搞定了以上的知识点之后,我们就可以依葫芦画瓢了。

我之前在项目中使用nacos,但是已有的nestjs的nacos插件我觉得不太行,我就自己编写了一个,相当于对以上知识也算是活学活用了,哈哈。

sicau-hsuyang/nestjs-nacos: the nacos integration in nestjs (github.com)

入口文件

因为我的这个插件必须要传入配置才可以使用,所以就不像@nestjs/axios那样可以支持静态模块调用了。

ts 复制代码
import { DynamicModule } from "@nestjs/common";
import { NacosConfigService } from "./config.service";
import { NacosConfigOptions } from "./interfaces/config.options";
import { NACOS_CONFIG_OPTIONS } from "./config.constants";

export class NacosConfigModule {
  static register(options: NacosConfigOptions): DynamicModule {
    return {
      module: NacosConfigModule,
      providers: [
        NacosConfigService,
        {
          provide: NACOS_CONFIG_OPTIONS,
          useValue: options,
        },
      ],
      exports: [NacosConfigService],
    };
  }
}

export * from "./config.constants";
export * from "./config.service";

Service

以下仅仅展示了核心代码。

ts 复制代码
import { Inject } from "@nestjs/common";
import { NacosConfigClient } from "nacos";
import { NacosConfigOptions } from "./interfaces/config.options";
import { NACOS_CONFIG_OPTIONS } from "./config.constants";

export class NacosConfigService {
  private client: NacosConfigClient;

  public get nacosClient() {
    return this.client;
  }

  constructor(@Inject(NACOS_CONFIG_OPTIONS) protected readonly configOptions: NacosConfigOptions) {
    const timeout = this.configOptions.timeout || 3000;
    this.client = new NacosConfigClient({
      // 一些配置,透传给nacos
    });
  }

}

然后就是配置tsc的编译,打包,发布到npm即可,这些流程就不给大家演示了。

使用

使用我编写的这个插件,直接安装我的包名,然后引入即可。

ts 复制代码
import { NacosConfigModule } from "nestjs-nacos";
import { Module } from "@nestjs/common";
import { AppController } from "./app.controller";
import { AppService } from "./app.service";

@Module({
  imports: [
    NacosConfigModule.register({
      // 传入一些配置
    }),
  ],
  controllers: [AppController],
  providers: [AppService],
})
export class AppModule {}

在业务侧调用:

ts 复制代码
import { NacosConfigService } from "nestjs-nacos";
import { Injectable } from "@nestjs/common";

@Injectable()
export class AppService {
  constructor(protected nacosConfigService: NacosConfigService) {
    this.nacosConfigService.subscribeKeyItem({/* some params */});
  }

  getHello() {
    return this.nacosConfigService.getKeyItemConfig({/* some params */});
  }
}

总结

  • 动态模块可以使得NestJS的模块具有可编程性,动态模块的module是必选参数,返回的是指定的Module,其余都是可选参数。
  • 动态模块的注册方法必须是静态函数,虽然是任意的名称,但是希望遵循社区的规范,比如register,forRoot,forFeature等。
  • 自定义的Provider可以具备丰富的能力,其中useFactory选项可以支持异步任务,IoC容器在创建Provider的实例的时候,只有等待异步任务完成之后才能完成这一过程。
  • 结合动态模块和自定义Provider的能力,使得我们可以用来编写插件以丰富NestJS的生态系统。
相关推荐
一路向前的月光2 小时前
Vue2中的监听和计算属性的区别
前端·javascript·vue.js
长路 ㅤ   2 小时前
vue-live2d看板娘集成方案设计使用教程
前端·javascript·vue.js·live2d
Fan_web2 小时前
jQuery——事件委托
开发语言·前端·javascript·css·jquery
Jiaberrr3 小时前
Element UI教程:如何将Radio单选框的圆框改为方框
前端·javascript·vue.js·ui·elementui
安冬的码畜日常5 小时前
【D3.js in Action 3 精译_029】3.5 给 D3 条形图加注图表标签(上)
开发语言·前端·javascript·信息可视化·数据可视化·d3.js
太阳花ˉ5 小时前
html+css+js实现step进度条效果
javascript·css·html
john_hjy6 小时前
11. 异步编程
运维·服务器·javascript
风清扬_jd6 小时前
Chromium 中JavaScript Fetch API接口c++代码实现(二)
javascript·c++·chrome
丁总学Java6 小时前
微信小程序-npm支持-如何使用npm包
前端·微信小程序·npm·node.js