前言
从这篇文章开始,我将不会再按一些既定的顺序给大家阐述自己的一些经验了,未来的文章将会是比较自由的,想到什么就写什么,哈哈哈。
您如果认真学习完成上一篇文章之后,基本上熟练应用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 theuseFactory
syntax. The factory returns aPromise
, and the factory function canawait
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 useHttpModule.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 oneGraphQLModule.forRoot()
, oneTypeOrmModule.forRoot()
, etc.forFeature
, you are expecting to use the configuration of a dynamic module'sforRoot
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
, andforFeatureAsync
, 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_TOKEN
给IoC
容器的时候,多了一个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的生态系统。