聊聊 nestjs 中的依賴注入
本文首發(fā)于政采云前端團(tuán)隊(duì)博客:聊聊 nestjs 中的依賴注入
https://zoo.team/article/nestjs

前言
首先 nestjs 是什么?引用其官網(wǎng)的原話 A progressive Node.js framework for building efficient, reliable and scalable server-side applications.,翻譯一下就是:“一個(gè)可以用來搭建高效、可靠且可擴(kuò)展的服務(wù)端應(yīng)用的 node 框架”。目前在 github 上有 42.4k 的 star 數(shù),人氣還是很高的。
在使用過程中會(huì)發(fā)現(xiàn) nest 框架和后端同學(xué)使用的 Springboot 以及前端三大框架之一的 Angular 都有很多相似之處。沒錯(cuò)這三個(gè)框架都有相似的設(shè)計(jì),并都實(shí)現(xiàn)了依賴注入。
可能對(duì)大部分前端同學(xué)來說,依賴注入這個(gè)詞還比較陌生,本文就圍繞依賴注入這個(gè)話題,展開討論一下依賴注入是什么?以及在 nestjs 中詳細(xì)的實(shí)現(xiàn)過程。
重要概念
概念解釋
先來看看幾個(gè)重要概念的解釋
依賴倒置原則( DIP ):抽象不應(yīng)該依賴實(shí)現(xiàn),實(shí)現(xiàn)也不應(yīng)該依賴實(shí)現(xiàn),實(shí)現(xiàn)應(yīng)該依賴抽象。 依賴注入(dependency injection,簡(jiǎn)寫為 DI):依賴是指依靠某種東西來獲得支持。將創(chuàng)建對(duì)象的任務(wù)轉(zhuǎn)移給其他class,并直接使用依賴項(xiàng)的過程,被稱為“依賴項(xiàng)注入”。 控制反轉(zhuǎn)(Inversion of Control, 簡(jiǎn)寫為 IoC):指一個(gè)類不應(yīng)靜態(tài)配置其依賴項(xiàng),應(yīng)由其他一些類從外部進(jìn)行配置。
結(jié)合代碼
光看上面的解釋可能并不好理解?那么我們把概念和具體的代碼結(jié)合起來看。
根據(jù) nest 官網(wǎng)教程,用腳手架創(chuàng)建一個(gè)項(xiàng)目,創(chuàng)建好的項(xiàng)目中有 main.ts 文件為入口文件,引入了 app.module.ts 文件,而 app.module.ts 文件引入了 app.controller.ts。先看一下代碼的邏輯:
???//?src/main.ts文件
???import?{?NestFactory?}?from?'@nestjs/core';
???import?{?AppModule?}?from?'./app.module';
???
???async?function?bootstrap()?{
?????const?app?=?await?NestFactory.create(AppModule);
?????await?app.listen(3000);
???}
???bootstrap();
???//?src/app.module.ts文件
???import?{?Module?}?from?'@nestjs/common';
???import?{?AppController?}?from?'./app.controller';
???import?{?AppService?}?from?'./app.service';?
???
???@Module({
?????imports:?[],
?????controllers:?[AppController],
?????providers:?[AppService],
???})
???export?class?AppModule?{}
???//?src/app.controller.ts文件
???import?{?Controller,?Get?}?from?'@nestjs/common';
???import?{?AppService?}?from?'./app.service';
???
???@Controller()
???export?class?AppController?{
?????constructor(private?readonly?appService:?AppService)?{}
???
?????@Get()
?????getHello():?string?{
???????return?this.appService.getHello();
?????}
???}
???//?src/app.service.ts文件
???import?{?Injectable?}?from?'@nestjs/common';
???
???@Injectable()
???export?class?AppService?{
?????getHello():?string?{
???????return?'Hello?World!';
?????}
???}
???
現(xiàn)在我們執(zhí)行 npm start 啟動(dòng)服務(wù),訪問 localhost:3000 就會(huì)執(zhí)行這個(gè) AppController 類中的 getHello 方法了。我們來看 app.controller.ts 文件。可以看到構(gòu)造函數(shù)的參數(shù)簽名中第一個(gè)參數(shù) appService 是 AppService 的一個(gè)實(shí)例。
constructor(private?readonly?appService:?AppService){}
但是在代碼里并有沒有看到實(shí)例化這個(gè) AppService 的地方。這里其實(shí)是把創(chuàng)建這個(gè)實(shí)例對(duì)象的工作交給了 nest 框架,而不是 AppController 自己來創(chuàng)建這個(gè)對(duì)象,這就是所謂的控制反轉(zhuǎn)。而把創(chuàng)建好的 AppService 實(shí)例對(duì)象作為 AppController 實(shí)例化時(shí)的參數(shù)傳給構(gòu)造器就是依賴注入了。
依賴注入的方式
依賴注入的實(shí)現(xiàn)主要有三種方式
構(gòu)造器注入:依賴關(guān)系通過 class 構(gòu)造器提供; setter 注入:用 setter 方法注入依賴項(xiàng); 接口注入:依賴項(xiàng)提供一個(gè)注入方法,該方法將把依賴項(xiàng)注入到傳遞給它的任何客戶端中。客戶端必須實(shí)現(xiàn)一個(gè)接口,該接口的 setter 方法接收依賴;在 nest 中采用了第一種方式——構(gòu)造器注入。
優(yōu)點(diǎn)
那么 nestjs 框架用了依賴注入和控制反轉(zhuǎn)有什么好處呢?
其實(shí) DI 和 IoC 是實(shí)現(xiàn)依賴倒置原則的具體手段。依賴倒置原則是設(shè)計(jì)模式五大原則(SOLID)中的第五項(xiàng)原則,也許上面這個(gè) AppController 的例子還看不出 DIP 有什么用,因?yàn)?DIP 也不是今天的重點(diǎn),這里就不多贅述了,但是通過上面的例子我們至少能體會(huì)到以下兩個(gè)優(yōu)點(diǎn):
減少樣板代碼,不需要再在業(yè)務(wù)代碼中寫大量實(shí)例化對(duì)象的代碼了; 可讀性和可維護(hù)性更高了,松耦合,高內(nèi)聚,符合單一職責(zé)原則,一個(gè)類應(yīng)該專注于履行其職責(zé),而不是創(chuàng)建履行這些職責(zé)所需的對(duì)象。
元數(shù)據(jù)反射
我們都知道 ts 中的類型信息是在運(yùn)行時(shí)是不存在的,那運(yùn)行時(shí)是如何根據(jù)參數(shù)的類型注入對(duì)應(yīng)實(shí)例的呢?
答案就是:元數(shù)據(jù)反射
先說反射,反射就是在運(yùn)行時(shí)動(dòng)態(tài)獲取一個(gè)對(duì)象的一切信息:方法/屬性等等,特點(diǎn)在于動(dòng)態(tài)類型反推導(dǎo)。不管是在 ts 中還是在其他類型語言中,反射的本質(zhì)在于元數(shù)據(jù)。在 TypeScript 中,反射的原理是通過編譯階段對(duì)對(duì)象注入元數(shù)據(jù)信息,在運(yùn)行階段讀取注入的元數(shù)據(jù),從而得到對(duì)象信息。
元數(shù)據(jù)反射(Reflect Metadata) 是 ES7 的一個(gè)提案,它主要用來在聲明的時(shí)候添加和讀取元數(shù)據(jù)。TypeScript 在 1.5+ 的版本已經(jīng)支持它。要在 ts 中啟用元數(shù)據(jù)反射相關(guān)功能需要:
npm i reflect-metadata --save。在 tsconfig.json里配置emitDecoratorMetadata選項(xiàng)為true。
定義元數(shù)據(jù)
Reflect.defineMetadata(metadataKey, data, target)
可以定義一個(gè)類的元數(shù)據(jù);
獲取元數(shù)據(jù)
Reflect.getMetadata(metadataKey, target),Reflect.getMetadata(metadataKey, instance, methodName)
可以獲取類或者方法上定義的元數(shù)據(jù)。
內(nèi)置元數(shù)據(jù)
TypeScript 結(jié)合自身語言的特點(diǎn),為使用了裝飾器的代碼聲明注入了 3 組元數(shù)據(jù):
design:type:成員類型design:paramtypes:成員所有參數(shù)類型design:returntype:成員返回類型
示例一:元數(shù)據(jù)的定義與獲取
import?'reflect-metadata';
class?A?{
??sayHi()?{
????console.log('hi');
??}
}
class?B?{
??sayHello()?{
????console.log('hello');
??}
}
function?Module(metadata)?{
??const?propsKeys?=?Object.keys(metadata);
??return?(target)?=>?{
????for?(const?property?in?metadata)?{
??????if?(metadata.hasOwnProperty(property))?{
????????Reflect.defineMetadata(property,?metadata[property],?target);
??????}
????}
??};
}
@Module({
??controllers:?[B],
??providers:?[A],
})
class?C?{}
const?providers?=?Reflect.getMetadata('providers',?C);
const?controllers?=?Reflect.getMetadata('controllers',?C);
console.log(providers,?controllers);?//?[?[class?A]?]?[?[class?B]?]
(new?(providers[0])).sayHi();?//?'hi'
在這個(gè)例子里,我們定義了一個(gè)名為 Module 的裝飾器,這個(gè)裝飾器的主要作用就是往裝飾的類上添加一些元數(shù)據(jù)。然后用裝飾器裝飾 C 類。我們就可以獲取到這個(gè)參數(shù)中的信息了;
示例二:依賴注入的簡(jiǎn)單實(shí)現(xiàn)
import?'reflect-metadata';
type?Constructorany>?=?new?(...args:?any[])?=>?T;
const?Test?=?():?ClassDecorator?=>?(target)?=>?{};
class?OtherService?{
??a?=?1;
}
@Test()
class?TestService?{
??constructor(public?readonly?otherService:?OtherService)?{}
??testMethod()?{
????console.log(this.otherService.a);
??}
}
const?Factory?=?(target:?Constructor):?T?=>?{
??//?獲取所有注入的服務(wù)
??const?providers?=?Reflect.getMetadata('design:paramtypes',?target);?//?[OtherService]
??const?args?=?providers.map((provider:?Constructor)?=>?new?provider());
??return?new?target(...args);
};
Factory(TestService).testMethod();?//?1
這里例子就是依賴注入簡(jiǎn)單的示例,這里 Test 裝飾器雖然什么都沒做,但是如上所說,只要使用了裝飾器,ts 就會(huì)默認(rèn)給類或?qū)?yīng)方法添加design:paramtypes的元數(shù)據(jù),這樣就可以通過Reflect.getMetadata('design:paramtypes', target)拿到類型信息了。
nest 中的實(shí)現(xiàn)
下面來看 nest 框架內(nèi)部是怎么來實(shí)現(xiàn)的
執(zhí)行邏輯
在入口文件 main.ts 中有這樣一行代碼
const?app?=?await?NestFactory.create(AppModule);
在源碼 nest/packages/core/nest-application.ts 找到 NestFactory.create 方法,這里用注釋解釋說明了與依賴注入相關(guān)的幾處代碼(下同)。
public?async?createextends?INestApplication?=?INestApplication>(
????module:?any,
????serverOrOptions?:?AbstractHttpAdapter?|?NestApplicationOptions,
????options?:?NestApplicationOptions,
??):?Promise?{
????const?[httpServer,?appOptions]?=?this.isHttpServer(serverOrOptions)
????????[serverOrOptions,?options]
??????:?[this.createHttpAdapter(),?serverOrOptions];
????const?applicationConfig?=?new?ApplicationConfig();
????//?1.?實(shí)例化?IoC?容器,這個(gè)容器就是用來存放所有對(duì)象的地方
????const?container?=?new?NestContainer(applicationConfig);?
????this.setAbortOnError(serverOrOptions,?options);
????this.registerLoggerConfiguration(appOptions);
????//?2.?執(zhí)行初始化邏輯,是依賴注入的核心邏輯所在
????await?this.initialize(module,?container,?applicationConfig,?httpServer);?
????
????//?3.?實(shí)例化?NestApplication?類
????const?instance?=?new?NestApplication(?????
??????container,
??????httpServer,
??????applicationConfig,
??????appOptions,
????);
????const?target?=?this.createNestInstance(instance);
????//?4.?生成一個(gè)?Proxy?代理對(duì)象,將對(duì)?NestApplication?實(shí)例上部分屬性的訪問代理到?httpServer,在?nest?中httpServer?默認(rèn)就是?express?實(shí)例對(duì)象,所以默認(rèn)情況下,express?的中間件都是可以使用的
????return?this.createAdapterProxy(target,?httpServer);?
??}
IoC 容器
在目錄 nest/packages/core/injector/container.ts,找到了 NestContainer 類,里面有很多成員屬性和方法,可以看到其中的私有屬性 modules 是一個(gè) ModulesContainer 實(shí)例對(duì)象,而 ModulesContainer 類是 Map 類的一個(gè)子類。
export?class?NestContainer?{???
??...
??private?readonly?modules?=?new?ModulesContainer();
??...
}
export?class?ModulesContainer?extends?Map<string,?Module>?{
???private?readonly?_applicationId?=?uuid();
??
???get?applicationId():?string?{
?????return?this._applicationId;
???}
}
依賴注入過程
先來看 this.initialize 方法:
??private?async?initialize(
????module:?any,
????container:?NestContainer,
????config?=?new?ApplicationConfig(),
????httpServer:?HttpServer?=?null,
??)?{
??//?1.?實(shí)例加載器
????const?instanceLoader?=?new?InstanceLoader(container);??
????const?metadataScanner?=?new?MetadataScanner();?????
????//?2.?依賴掃描器
????const?dependenciesScanner?=?new?DependenciesScanner(???
??????container,
??????metadataScanner,
??????config,
????);
????container.setHttpAdapter(httpServer);
????const?teardown?=?this.abortOnError?===?false???rethrow?:?undefined;
????await?httpServer?.init();
????try?{
??????this.logger.log(MESSAGES.APPLICATION_START);
??????await?ExceptionsZone.asyncRun(
????????async?()?=>?{
??????????//?3.?掃描依賴
??????????await?dependenciesScanner.scan(module);?
??????????//?4.?生成依賴的實(shí)例
??????????await?instanceLoader.createInstancesOfDependencies();?
??????????dependenciesScanner.applyApplicationProviders();
????????},
????????teardown,
????????this.autoFlushLogs,
??????);
????}?catch?(e)?{
??????this.handleInitializationError(e);
????}
??}
new InstanceLoader()實(shí)例化 InstanceLoader 類,并把剛才的 IoC 容器作為參數(shù)傳入,這個(gè)類是專門用來生成需要注入的實(shí)例對(duì)象的;實(shí)例化 MetadataScanner 類和 DependenciesScanner 類,MetadataScanner 類是一個(gè)用來獲取 元數(shù)據(jù)的工具類,而 DependenciesScanner 類是用來掃描出所有 modules 中的依賴項(xiàng)的。上面的 app.module.ts 中 Module 裝飾器的參數(shù)中傳入了controllers、providers等其他選項(xiàng),這個(gè) Module 裝飾器的作用就是標(biāo)明 AppModule 類的一些依賴項(xiàng);
???@Module({
?????imports:?[],
?????controllers:?[AppController],
?????providers:?[AppService],
???})
???export?class?AppModule?{}
調(diào)用依賴掃描器的 scan 方法,掃描依賴;
???public?async?scan(module:?Type)?{
?????await?this.registerCoreModule();?//?1.?把一些內(nèi)建module添加到IoC容器中
?????await?this.scanForModules(module);?//?2.?把傳入的module添加到IoC容器中
?????await?this.scanModulesForDependencies();?//?3.?掃描當(dāng)前IoC容器中所有module的依賴
?????this.calculateModulesDistance();
???
?????this.addScopedEnhancersMetadata();
?????this.container.bindGlobalScope();
???}
這里所說的 module 可以理解為是模塊,但并不是 es6 語言中的模塊化的 module,而是 app.module.ts 中定義的類, 而 nest 內(nèi)部也有一個(gè)內(nèi)建的 Module 類,框架會(huì)根據(jù) app.module.ts 中定義的 module 類去實(shí)例化一個(gè)內(nèi)建的 Moudle 類。下面 addModule 方法是把 module 添加到 IoC 容器的方法,可以看到,這里針對(duì)每個(gè) module 會(huì)生成一個(gè) token,然后實(shí)例化內(nèi)建的 Module 類,并放到容器的 modules 屬性上,token 作為 Map 結(jié)構(gòu)的 key,Module 實(shí)例作為值。
?public?async?addModule(
???metatype:?Type<any>?|?DynamicModule?|?Promise,
???scope:?Type<any>[],
?):?Promiseundefined>?{
???//?In?DependenciesScanner#scanForModules?we?already?check?for?undefined?or?invalid?modules
???//?We?still?need?to?catch?the?edge-case?of?`forwardRef(()?=>?undefined)`
???if?(!metatype)?{
?????throw?new?UndefinedForwardRefException(scope);
???}
???const?{?type,?dynamicMetadata,?token?}?=?await?this.moduleCompiler.compile(
?????metatype,
???);?//?生成token
???if?(this.modules.has(token))?{
?????return?this.modules.get(token);
???}
???const?moduleRef?=?new?Module(type,?this);?//?實(shí)例化內(nèi)建Module類
???moduleRef.token?=?token;
???this.modules.set(token,?moduleRef);?//?添加在modules上
???await?this.addDynamicMetadata(
?????token,
?????dynamicMetadata,
?????[].concat(scope,?type),
???);
???if?(this.isGlobalModule(type,?dynamicMetadata))?{
?????this.addGlobalModule(moduleRef);
???}
???return?moduleRef;
?}
scanModulesForDependencies方法會(huì)找到容器中每個(gè) module 上的一些元數(shù)據(jù),把對(duì)應(yīng)的元數(shù)據(jù)分別添加到剛才添加到容器中的 module 上面,這些元數(shù)據(jù)就是根據(jù)上面提到的 Module ?裝飾器的參數(shù)生成的;instanceLoader.createInstancesOfDependencies()
private?async?createInstances(modules:?Map<string,?Module>)?{
?????await?Promise.all(
???????[...modules.values()].map(async?moduleRef?=>?{
?????????await?this.createInstancesOfProviders(moduleRef);
?????????await?this.createInstancesOfInjectables(moduleRef);
?????????await?this.createInstancesOfControllers(moduleRef);
?
?????????const?{?name?}?=?moduleRef.metatype;
?????????this.isModuleWhitelisted(name)?&&
???????????this.logger.log(MODULE_INIT_MESSAGE`${name}`);
???????}),
?????);
??}
遍歷 modules 然后生成 provider、Injectable、controller 的實(shí)例。生成實(shí)例的順序上也是有講究的,controller 是最后生成的。在生成實(shí)例的過程中,nest 還會(huì)先去找到構(gòu)造器中的依賴項(xiàng):
const?dependencies?=?isNil(inject)?
????this.reflectConstructorParams(wrapper.metatype?as?Type<any>)?
??:?inject;
reflectConstructorParams(type:?Type):?any[]?{
?????const?paramtypes?=?Reflect.getMetadata(PARAMTYPES_METADATA,?type)?||?[];
?????const?selfParams?=?this.reflectSelfParams(type);
?
?????selfParams.forEach(({?index,?param?})?=>?(paramtypes[index]?=?param));
?????return?paramtypes;
?}
上面代碼中的的常量 PARAMTYPES_METADATA就是 ts 中內(nèi)置的;metadataKeydesign:paramtypes,獲取到構(gòu)造參數(shù)類型信息;然后就可以先實(shí)例化依賴項(xiàng);
async?instantiateClass(instances,?wrapper,?targetMetatype,?contextId?=?constants_2.STATIC_CONTEXT,?inquirer)?{
?????????const?{?metatype,?inject?}?=?wrapper;
?????????const?inquirerId?=?this.getInquirerId(inquirer);
?????????const?instanceHost?=?targetMetatype.getInstanceByContextId(contextId,?inquirerId);
?????????const?isInContext?=?wrapper.isStatic(contextId,?inquirer)?||
?????????????wrapper.isInRequestScope(contextId,?inquirer)?||
?????????????wrapper.isLazyTransient(contextId,?inquirer)?||
?????????????wrapper.isExplicitlyRequested(contextId,?inquirer);
?????????if?(shared_utils_1.isNil(inject)?&&?isInContext)?{
?????????????instanceHost.instance?=?wrapper.forwardRef
???????????????????Object.assign(instanceHost.instance,?new?metatype(...instances))
?????????????????:?new?metatype(...instances);
?????????}
?????????else?if?(isInContext)?{
?????????????const?factoryReturnValue?=?targetMetatype.metatype(...instances);
?????????????instanceHost.instance?=?await?factoryReturnValue;
?????????}
?????????instanceHost.isResolved?=?true;
?????????return?instanceHost.instance;
?}
依賴項(xiàng)全部實(shí)例化后再調(diào)用 instantiateClass方法,依賴項(xiàng)作為第一個(gè)參數(shù) instances 傳入。這里的new metatype(...instances)把依賴項(xiàng)的實(shí)例作為參數(shù)全部傳入。
執(zhí)行流程圖
NestFactory.create 方法的執(zhí)行邏輯大概如下

總結(jié)
元數(shù)據(jù)反射是實(shí)現(xiàn)依賴注入的基礎(chǔ); 總結(jié)依賴注入的過程,nest 主要做了三件事情 知道哪些類需要哪些對(duì)象 創(chuàng)建對(duì)象 并提供所有這些對(duì)象
參考
nestjs官方文檔 (https://docs.nestjs.com) 深入理解Typescript——Reflect Metadata (https://jkchao.github.io/typescript-book-chinese/tips/metadata.html#%E5%9F%BA%E7%A1%80) Dependency injection in Angular (https://angular.io/guide/dependency-injection) 裝飾器 (https://www.typescriptlang.org/docs/handbook/decorators.html) 從 JavaScript 到 TypeScript 4 - 裝飾器和反射 (https://segmentfault.com/a/1190000011520817) 反射的本質(zhì)——元數(shù)據(jù) (https://developer.aliyun.com/article/382120) 《大話設(shè)計(jì)模式》——程杰

往期推薦



最后
歡迎加我微信,拉你進(jìn)技術(shù)群,長(zhǎng)期交流學(xué)習(xí)...
歡迎關(guān)注「前端Q」,認(rèn)真學(xué)前端,做個(gè)專業(yè)的技術(shù)人...


