Nest.js 之依賴注入原理及實現(xiàn)過程詳解
前言
很久之前初學 Java
時就對注解及自動依賴注入這種方式感覺到不可思議,但是一直沒有勇氣(懶)去搞清楚。現(xiàn)在做前端了,發(fā)現(xiàn) Nest.js 里面竟然也是這玩意,終究還是躲不過,那就趁著“陽康”了搞清楚一下吧。
關(guān)于為什么要進行依賴注入這里就不展開了,下面直接進入正題,TypeScript 依賴注入的原理。
TypeScript 依賴注入的原理
TypeScript 中實現(xiàn)依賴注入離不開 Decorator
和 Metadata
(需要引入第三方庫 reflect-metadata
),下面通過一個簡單的例子來快速了解它的用途:
import 'reflect-metadata' @Reflect.metadata('class', 'Class Data') class Test { @Reflect.metadata('method', 'Method Data') public hello(): string { return 'hello world' } } console.log(Reflect.getMetadata('class', Test)) // Class Data console.log(Reflect.getMetadata('method', new Test(), 'hello')) // Method Data
通過例子可以看到,我們通過 Reflect.metadata()
這個裝飾器可以往類及其方法上面添加數(shù)據(jù),然后通過 Reflect.getMetadata
可以取到這些數(shù)據(jù)。我們可以借助這一特性,實現(xiàn)簡單的依賴注入:
import 'reflect-metadata' class TestService {} @Reflect.metadata('params', [TestService]) class Test { constructor(testService: TestService) {} public hello(): string { return 'hello world' } } type Constructor<T = any> = new (...args: any[]) => T const inject = <T>(target: Constructor<T>): T => { const providers = Reflect.getMetadata('params', target) const args = providers.map((provider: Constructor) => new provider()) return new target(...args) } inject(Test).hello()
如上所示,我們通過 @Reflect.metadata('params', [TestService])
在 Test
上添加了元數(shù)據(jù),表示構(gòu)造函數(shù)中需要用到 TestService
,但 Nest.js
中好像不需要這樣。怎么辦呢?答案就是:
{ "compilerOptions": { "emitDecoratorMetadata": true } }
開啟了這個參數(shù)后,我們就不需要手動添加元數(shù)據(jù)了:
import 'reflect-metadata' class TestService {} const D = (): ClassDecorator => (target) => {} @D() class Test { constructor(testService: TestService) {} public hello(): string { return 'hello world' } } type Constructor<T = any> = new (...args: any[]) => T const inject = <T>(target: Constructor<T>): T => { const providers = Reflect.getMetadata('design:paramtypes', target) const args = providers.map((provider: Constructor) => new provider()) return new target(...args) } inject(Test).hello()
原因在于開啟 emitDecoratorMetadata
后,TS 自動會在我們的裝飾器前添加一些裝飾器。比如,下面這段代碼:
import 'reflect-metadata' const D = (): ClassDecorator => (target) => {} const methodDecorator = (): MethodDecorator => (target, key, descriptor) => {} @D() class Test { constructor(a: number) {} @methodDecorator() public hello(): string { return 'hello world' } public hi() {} }
編譯過后是這樣子的:
var __decorate = ... var __metadata = ... import 'reflect-metadata' const D = () => (target) => {} const methodDecorator = () => (target, key, descriptor) => {} let Test = class Test { constructor(a) {} hello() { return 'hello world' } hi() {} } __decorate( [ methodDecorator(), __metadata('design:type', Function), __metadata('design:paramtypes', []), __metadata('design:returntype', String), ], Test.prototype, 'hello', null ) Test = __decorate( [D(), __metadata('design:paramtypes', [Number])], Test )
可以看到,TS 自動會添加 design:type|paramtypes|returntype
三種類型的元數(shù)據(jù),分別表示目標本身,參數(shù)以及返回值的類型。
我們把 inject
稍微改一下,支持遞歸的注入,這樣一個簡單的依賴注入就實現(xiàn)了:
import 'reflect-metadata' const D = (): ClassDecorator => (target) => {} class OtherService {} @D() class TestService { constructor(otherService: OtherService) {} } @D() class Test { constructor(testService: TestService) {} public hello(): string { return 'hello world' } } type Constructor<T = any> = new (...args: any[]) => T const inject = <T>(target: Constructor<T>): T => { const providers = Reflect.getMetadata('design:paramtypes', target) if (providers) { const args = providers.map((provider: Constructor) => { return inject(provider) }) return new target(...args) } return new target() } inject(Test).hello()
接下來,我們淺看一下 Nest.js
大概是怎么實現(xiàn)的。
淺析 Nest.js 實現(xiàn)依賴注入的過程
我們通過官方腳手架生成一個 Demo 項目,可以發(fā)現(xiàn)其中 tsconfig.json
中的 emitDecoratorMetadata
確實是開啟的。我們先用一個最簡單的例子來說明:
// app.module.ts import {Injectable, Module} from '@nestjs/common' @Injectable() class TestService { hello() { return 'hello world' } } @Module({ providers: [TestService], }) export class AppModule { constructor(testService: TestService) { testService.hello() } } // main.ts import {NestFactory} from '@nestjs/core' import {AppModule} from './app.module' async function bootstrap() { await NestFactory.create(AppModule) } bootstrap()
為了更加直觀的理解流程,這里暫時先把源碼核心部分扒下來,我們把 await NestFactory.create(AppModule)
替換成我們自己的代碼:
const injector = new Injector() await injector.inject(AppModule)
import {Type} from '@nestjs/common' import {MODULE_METADATA} from '@nestjs/common/constants' import {ApplicationConfig, NestContainer} from '@nestjs/core' import {InstanceLoader} from '@nestjs/core/injector/instance-loader' export default class Injector { container: NestContainer public async inject(module: any) { const applicationConfig = new ApplicationConfig() this.container = new NestContainer(applicationConfig) const moduleInstance = await this.container.addModule(module, null) // 1 resolve dependencies const {token, metatype} = moduleInstance this.reflectProviders(metatype, token) // 2 create instance const instanceLoader = new InstanceLoader(this.container) instanceLoader.createInstancesOfDependencies() } public reflectProviders(module: Type<any>, token: string) { const providers = [ ...this.reflectMetadata(MODULE_METADATA.PROVIDERS, module), ] providers.forEach((provider) => { return this.container.addProvider(provider as Type<any>, token) }) } public reflectMetadata(metadataKey: string, metatype: Type<any>) { return Reflect.getMetadata(metadataKey, metatype) || [] } }
這里大概分成兩部分:
- 處理
module
的依賴,也就是@Module
裝飾器所聲明的,我們這里暫時只考慮providers
。這一步執(zhí)行完后,NestContainer
中數(shù)據(jù)如下(注意到Module
本身也作為自己的provider
):
{ modules: { '19bb8f429cacdbcc18fc1afcaac891a4606578aa': Module { _metatype: class AppModule {...}, _providers: { class AppModule {...}: InstanceWrapper {}, // Module 本身也作為自己的 provider class TestService {...}: InstanceWrapper {} } } } }
- 實例化
Module
。這一部分需要稍微看一下源碼:
public async createInstancesOfDependencies( modules: Map<string, Module> = this.container.getModules(), ) { ... await this.createInstances(modules); } ... private async createInstances(modules: Map<string, Module>) { await Promise.all( [...modules.values()].map(async moduleRef => { await this.createInstancesOfProviders(moduleRef); ... }), ); }
這里的意思是實例化所有的 Module
,實例化 Module
前,我們需要先實例化它的依賴,具體到這里就是實例化 providers
:
private async createInstancesOfProviders(moduleRef: Module) { const { providers } = moduleRef; const wrappers = [...providers.values()]; await Promise.all( wrappers.map(item => this.injector.loadProvider(item, moduleRef)), ); }
最后會到 loadInstance
這個函數(shù):
public async loadInstance<T>( wrapper: InstanceWrapper<T>, collection: Map<InstanceToken, InstanceWrapper>, moduleRef: Module, contextId = STATIC_CONTEXT, inquirer?: InstanceWrapper, ) { ... try { const callback = async (instances: unknown[]) => { const properties = await this.resolveProperties( wrapper, moduleRef, inject as InjectionToken[], contextId, wrapper, inquirer, ); const instance = await this.instantiateClass( instances, wrapper, targetWrapper, contextId, inquirer, ); this.applyProperties(instance, properties); done(); }; await this.resolveConstructorParams<T>( wrapper, moduleRef, inject as InjectionToken[], callback, contextId, wrapper, inquirer, ); } catch (err) { done(err); throw err; } }
接下來就到了最重要的 this.resolveConstructorParams
這個函數(shù)了,我們以 class AppModule
這個 provider
為例來分析:
public async resolveConstructorParams<T>( wrapper: InstanceWrapper<T>, moduleRef: Module, inject: InjectorDependency[], callback: (args: unknown[]) => void | Promise<void>, contextId = STATIC_CONTEXT, inquirer?: InstanceWrapper, parentInquirer?: InstanceWrapper, ) { // dependencies 返回的就是 AppModule 構(gòu)造函數(shù)的參數(shù)類型,本例為: [TestService] const [dependencies, optionalDependenciesIds] = isFactoryProvider ? this.getFactoryProviderDependencies(wrapper) : this.getClassDependencies(wrapper); let isResolved = true; const resolveParam = async (param: unknown, index: number) => { ... }; // 這里的 instances 就是通過 dependencies 實例化后的對象,具體到本例,可以理解為這樣: [new TestService()] const instances = await Promise.all(dependencies.map(resolveParam)); isResolved && (await callback(instances)); }
其中調(diào)用 this.getClassDependencies(wrapper)
最終會調(diào)用 reflectConstructorParams
:
public reflectConstructorParams<T>(type: Type<T>): any[] { const paramtypes = Reflect.getMetadata(PARAMTYPES_METADATA, type) || []; const selfParams = this.reflectSelfParams<T>(type); selfParams.forEach(({ index, param }) => (paramtypes[index] = param)); return paramtypes; }
這里的 PARAMTYPES_METADATA
就是 design:paramtypes
。
終于看到了我們想要的結(jié)果,那本文暫時就分析到這里吧,這樣一次帶著一個問題看源碼,目標明確,不至于陷入源碼的汪洋大海之中。
總結(jié)
本文先通過幾個簡單的例子揭示了 TS 中如何實現(xiàn)依賴注入,核心原理在于通過 Decorator
及 Metadata
兩大特性可以在類及其方法上存儲一些數(shù)據(jù),并且開啟了 emitDecoratorMetadata
后,TS 還可以自動添加三種類型的數(shù)據(jù)。
然后簡單地調(diào)試了 Nest.js
的初始化過程,發(fā)現(xiàn)原理與我們分析的類似。
以上就是Nest.js 之依賴注入原理及實現(xiàn)過程詳解的詳細內(nèi)容,更多關(guān)于Nest.js 依賴注入原理的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
codemirror6實現(xiàn)在線代碼編輯器使用詳解
這篇文章主要為大家介紹了codemirror6實現(xiàn)在線代碼編輯器使用詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪2023-01-01JavaScript數(shù)據(jù)類型對函數(shù)式編程的影響示例解析
這篇文章主要為大家介紹了JavaScript數(shù)據(jù)類型對函數(shù)式編程的影響示例解析,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪2023-02-02