@Module装饰器接受一个对象参数,在Nest源码中是ModuleMetadata接口,它有四个字段,且均是数组类型,分别是:
通过以上四种类型的设定,Nest的IoC才能够准确识别需要组装(注入和被注入)各种依赖关系,同时实现一定程度的共享。
SOLID指面向对象编程中的五项基本原则。应用这些原则,可以提升程序的可维护行和扩展性,同时也让大规模协作开发变得可能。表格源自WiKi:
模块的设计遵循SOLID原则。另外,在Nest中:一组功能也会被归类到一个文件夹中,同时体现在程序文件名的前缀上。
nestgmouser内容如下:
@Module({controllers:[UserController],providers:[AppService],})exportclassUserModule{}还需要修改app.module,引入user.module
@Module({imports:[UserModule],controllers:[AppController],providers:[AppService],})exportclassAppModule{}运行项目,然后依次执行下面的代码,查看输出:
npmrunstart:devcurl--location--requestGET'localhost:3000/setstr=Hi~'#donecurl--location--requestGET'localhost:3000/get'#Hi~curl--location--requestGET'localhost:3000/user/get'#curl--location--requestGET'localhost:3000/user/setstr=HiUser~'#donecurl--location--requestGET'localhost:3000/user/get'#HiUser~curl--location--requestGET'localhost:3000/get'#Hi~根据这个结果,不难推断出,AppService在AppModule和UserModule中是两个独立存在的实例。也许这并不符合我们的开发本意,而是需要使用一个实例。这就是Nest的模块共享的实质意义。
我们只需要将UserModule添加一行export[AppServie],将AppModule中的providers:[AppService]删去(反之亦然),即可实现功能的共享。然后再次执行上面的检查代码,看看输出结果。
先前在Provider章节中提到,Provider是一个比较宽泛的概念,不仅局限于Service类型,实际上任何一个类、值乃至一个接口,都可以视作一个Provider。在调用时也可以使调用时也可以用参数装饰器@Inject()来修饰后面的参数需要什么样的依赖。例如:
exportclassAppService{constructor(@Inject('MethodOptions')privateoption:MethodOptions){}}在应用Provider时,我们说一下Provider的几种类型,查看源代码:
在上级模块引入模块时,可以调用模块的静态方法,以获得一个DynamicModule对象,例如:
为了让源代码够简单,在最小的项目基础上只增加了User,两者在层级上有上下级关系,但两者关于调用Provider是一样的。有兴趣的同学可以在增加一个类型(例如Customer)装配在App的下面。用最简单的内容来举例,抽象类和两个实现(一共三个文件):
///src/common/common.abstract.tsexportabstractclassAbstractDBCommonService{setPrefix(value:string){}getPrefix():string{return'abstract';}}///src/common/user.db.service.tsimport{AbstractDBCommonService}from'./common.abstract';exportclassUserDBServiceimplementsAbstractDBCommonService{private_prefix:string='user';setPrefix(value:string){this._prefix=value;}getPrefix():string{returnthis._prefix;}}///src/common/app.db.service.tsimport{AbstractDBCommonService}from'./common.abstract';exportclassAppDBServiceimplementsAbstractDBCommonService{private_prefix:string='app';setPrefix(value:string){this._prefix=value;}getPrefix():string{returnthis._prefix;}}添加User和App对DB.Service的依赖,UserController的代码与AppController代码较为相似,两者不需要关心具体实现了什么:
///src/user/user.controller.tsimport{Controller,Get,Query}from'@nestjs/common';import{AppService}from'src/app.service';import{AbstractDBCommonService}from'src/common/common.abstract';@Controller('user')exportclassUserController{constructor(privatereadonlyappService:AppService,privatecommonService:AbstractDBCommonService,){}@Get('/')hello():string{returnthis.commonService.getPrefix()+this.appService.getHello();}}上面的代码中,构造函数需要有两个注入,AppSerivce和AbstractDBCommonService,我们可以将这两个Provider视作为某一类服务所共同需要的支持,在hello方法中,输出是这两个Service功能的整合。AppController文件与之基本保持一致。动态模块CommonModule的代码如下:
///src/common/common.module.tsimport{DynamicModule,Inject,Module}from'@nestjs/common';import{AppDBService}from'./app.db.service';import{AbstractDBCommonService}from'./common.abstract';import{UserDBService}from'./user.db.service';constWarehouse={AppDBService,UserDBService};@Module({})exportclassCommonModule{staticLoadByClass(kind:string):DynamicModule{constprovider={provide:AbstractDBCommonService,useClass:Warehouse[kind],};return{module:CommonModule,providers:[provider],exports:[provider],};}}然后是UserModule和AppModule
///src/user/user.module.tsimport{Module}from'@nestjs/common';import{AppService}from'../app.service';import{UserController}from'./user.controller';import{CommonModule}from'src/common/common.module';@Module({imports:[CommonModule.LoadByClass('UserDBService')],controllers:[UserController],providers:[AppService],})exportclassUserModule{}///src/app.module.tsimport{Module}from'@nestjs/common';import{AppController}from'./app.controller';import{AppService}from'./app.service';import{UserModule}from'./user/user.module';import{CommonModule}from'./common/common.module';@Module({imports:[UserModule,CommonModule.LoadByClass('AppDBService')],controllers:[AppController],providers:[AppService],})exportclassAppModule{}运行代码得到结果:
providers:[{provider:SomeService,useClass:SomeService}]值型的Provider官方的案例以及网上大多数的案例都是以值类型的Provider来演示的。基本原理是:不同的环境以确定不同的“值”,这个值是加载不同的配置文件(名)。
假设CommonService为UserController和AppController提供服务,另外,为了方便,需要安装一个dotenv组件(npminstall--savedotenv)。
///src/common/common.service.tsimport{Inject,Injectable}from'@nestjs/common';import*asdotenvfrom'dotenv';import*asfsfrom'fs';@Injectable()exportclassCommonService{privatereadonlyenv;constructor(@Inject('file')file:string){this.env=dotenv.parse(fs.readFileSync(file));}getPrefix(){returnthis.env['PREFIX'];}}接着定义两个配置文件/config/app.env和/config/user.env。注意,配置文件不能放在src目录下,因为ts文件会被编译为js文件,并且放在dist目录中执行,如果放在源代码目录中又没有利用webpack进行特定的打包时,就会发生无法找到文件的异常。文件内容及其简单:
PREFIX=APP我们将CommonModule文件稍加修改,另外用一个静态的方法LoadByValue:
///src/common/common.module.tsimport{DynamicModule,Inject,Module}from'@nestjs/common';import*aspathfrom'path';import{CommonService}from'./common.service';@Module({})exportclassCommonModule{staticLoadByValue(file:string):DynamicModule{constenvFile=path.resolve(__dirname,'../../config',file);return{module:CommonModule,providers:[{provide:'file',useValue:envFile},CommonService],exports:[CommonService],};}}接着将UserModule和AddModule的引用方式改为imports:[CommonModule.LoadENV('user.env')],以及相应的AppController和UserController的构造函数依赖:privatereadonlycommonService:CommonService。运行代码的结果:
///src/common/common.module.tsimport{DynamicModule,Inject,Module}from'@nestjs/common';import{AppDBService}from'./app.db.service';import{UserDBService}from'./user.db.service';constWarehouse={UserDBService,AppDBService};@Module({})exportclassCommonModule{staticLoadByFactory(kind:string):DynamicModule{constprovide={provide:kind,useFactory:()=>{returnnewWarehouse[kind]();},};return{module:CommonModule,providers:[provide],exports:[provide],};}}UserModule和AppModule改动
///src/app.module.tsimport{Module}from'@nestjs/common';import{AppController}from'./app.controller';import{AppService}from'./app.service';import{UserModule}from'./user/user.module';import{CommonModule}from'./common/common.module';@Module({imports:[UserModule,CommonModule.LoadByFactory('AppDBService')],controllers:[AppController],providers:[AppService],})exportclassAppModule{}UserController和AppController的改动。注意,第9和10行是等效的。
///src/app.controller.tsimport{Controller,Get,Inject,Put,Query}from'@nestjs/common';import{AppService}from'./app.service';@Controller()exportclassAppController{constructor(privatereadonlyappService:AppService,@Inject('AppDBService')privatereadonlycommonService,//等效privatereadonlycommonService:AppDBService,){}@Get()getHello():string{returnthis.commonService.Prefix+this.appService.getHello();}}被依赖的类型如果是同一个(将上文中App和User调整为同一个依赖项),你会发现他们共享了同一个实例。
@Module({})exportclassCommonModule{staticLoadByFactory(kind:string):DynamicModule{constprovide={provide:kind,useFactory:(a:A,b:B)=>{console.log(a,b);//->A{}B{}returnnewWarehouse[kind]();},inject:[A,B],};return{module:CommonModule,//imports:[TestModule],providers:[A,B,provide],exports:[provide],};}}注意,被依赖的A和B必须是作为Provider来看待,也就是说,要么在providers中,也可以是注册在其他module中的共享providers并导入(imports)在当前模块中。Nest会自动将对象实力化,并送入useFactory()中。
当使用useClass的动态模块组装模块时,每个模块都会实例化类并且私有使用。如果这个类需要被共享使用(单例),例如一个配置文件,那么就可以使用这个方式。需要注意的是:因为是一个别名,实际上并不存在于实际的代码中,所以在依赖方的代码中不能依靠类型限制来自动完成注入,而是利用@Inject()装饰器。
///src/app.controller.tsimport{Controller,Get,Inject,Put,Query}from'@nestjs/common';import{AppService}from'./app.service';@Controller()exportclassAppController{constructor(privatereadonlyappService:AppService,@Inject('AppDBService')privatereadonlycommonService,){}}同理UserController。CommonModule改为:
import{DynamicModule,Module}from'@nestjs/common';import{AbstractDBCommonService}from'./common.abstract';@Module({})exportclassCommonModule{staticloadByExisting():DynamicModule{constuserAliasConfigFile={provide:'UserDBService',useExisting:AbstractDBCommonService,};constappAliasConfigFile={provide:'AppDBService',useExisting:AbstractDBCommonService,};return{module:CommonModule,providers:[AbstractDBCommonService,userAliasConfigFile,appAliasConfigFile,],exports:[AbstractDBCommonService,userAliasConfigFile,appAliasConfigFile,],};}}执行测试程序可以发现,虽然是不同的注入项目,但最终依赖的AbstractDBCommonService是同一个实例。如果将*AliasConfigFile直接放在app.module.ts和user.module.ts的providers中,其实也是可以完成别名的依赖配置,但是两者使用的不同的实例。
以上动态模块根据接口主要分为四个不同的类别:
动态类同样是可以被输出共享(exports),导出需要使用于provider的provide字段关联。例如工厂模式中,定义一个{provide:'xxx',userFactory:()=>{...}},将其输出就是exports:['xxx'],其他同理。
在上文模块共享中提到过,Nest的策略遵循SOLID中的O,开闭原则:有限度的开放,所以一个模块除非是定义为输出共享,它才能被其他依赖方程序使用。假如某个模块几乎所有的地方都会被用到,那么可以利用@Global()装饰器,将你的模块定义为全局范围。如果是动态模块,那么在返回的DynamicModule中加入globa:true即可。
不论是@Global()静态模块还是global:true的动态模块,只需要且也必须注册一次,一般情况下可以放在根模块app.module.ts或者核心模块中即可。