在前面幾章中,我們討論了依賴注入(DI)的各個(gè)方面,以及如何在 Nest 中使用它。其中一個(gè)例子是基于構(gòu)造函數(shù)的依賴注入,用于將實(shí)例(通常是服務(wù)提供者)注入到類中。當(dāng)您了解到依賴注入是以一種基本的方式構(gòu)建到 Nest 內(nèi)核中時(shí),您不會(huì)感到驚訝。到目前為止,我們只探索了一個(gè)主要模式。隨著應(yīng)用程序變得越來越復(fù)雜,您可能需要利用 DI 系統(tǒng)的所有特性,因此讓我們更詳細(xì)地研究它們。
依賴注入是一種控制反轉(zhuǎn)(IoC)技術(shù),您可以將依賴的實(shí)例化委派給 IoC 容器(在我們的示例中為 NestJS 運(yùn)行時(shí)系統(tǒng)),而不是必須在自己的代碼中執(zhí)行。 讓我們從“提供者”一章中檢查此示例中發(fā)生的情況。
首先,我們定義一個(gè)提供者。@Injectable()裝飾器將 CatsService 類標(biāo)記為提供者。
cats.service.ts
import { Injectable } from '@nestjs/common';
import { Cat } from './interfaces/cat.interface';
@Injectable()
export class CatsService {
private readonly cats: Cat[] = [];
findAll(): Cat[] {
return this.cats;
}
}
然后,我們要求 Nest 將提供程序注入到我們的控制器類中:
cats.controller.ts
import { Controller, Get } from '@nestjs/common';
import { CatsService } from './cats.service';
import { Cat } from './interfaces/cat.interface';
@Controller('cats')
export class CatsController {
constructor(private readonly catsService: CatsService) {}
@Get()
async findAll(): Promise<Cat[]> {
return this.catsService.findAll();
}
}
最后,我們?cè)?nbsp;Nest IoC 容器中注冊(cè)提供程序
app.module.ts
import { Module } from '@nestjs/common';
import { CatsController } from './cats/cats.controller';
import { CatsService } from './cats/cats.service';
@Module({
controllers: [CatsController],
providers: [CatsService],
})
export class AppModule {}
這個(gè)過程有三個(gè)關(guān)鍵步驟:
1.在 cats.service.ts 中 @Injectable() 裝飾器聲明 CatsService 類是一個(gè)可以由Nest IoC容器管理的類。
2.在 cats.controller.ts 中 CatsController 聲明了一個(gè)依賴于 CatsService 令牌(token)的構(gòu)造函數(shù)注入:
constructor(private readonly catsService: CatsService)
3.在 app.module.ts 中,我們將標(biāo)記 CatsService與 cats.service.ts文件中的 CatsService 類相關(guān)聯(lián)。 我們將在下面確切地看到這種關(guān)聯(lián)(也稱為注冊(cè))的發(fā)生方式。
當(dāng) Nest IoC 容器實(shí)例化 CatsController 時(shí),它首先查找所有依賴項(xiàng)*。 當(dāng)找到 CatsService 依賴項(xiàng)時(shí),它將對(duì) CatsService令牌(token)執(zhí)行查找,并根據(jù)上述步驟(上面的#3)返回 CatsService 類。 假定單例范圍(默認(rèn)行為),Nest 然后將創(chuàng)建 CatsService 實(shí)例,將其緩存并返回,或者如果已經(jīng)緩存,則返回現(xiàn)有實(shí)例。
這個(gè)解釋稍微簡(jiǎn)化了一點(diǎn)。我們忽略的一個(gè)重要方面是,分析依賴項(xiàng)代碼的過程非常復(fù)雜,并且發(fā)生在應(yīng)用程序引導(dǎo)期間。一個(gè)關(guān)鍵特性是依賴關(guān)系分析(或“創(chuàng)建依賴關(guān)系圖”)是可傳遞的。 在上面的示例中,如果 CatsService 本身具有依賴項(xiàng),那么那些依賴項(xiàng)也將得到解決。 依賴關(guān)系圖確保以正確的順序解決依賴關(guān)系-本質(zhì)上是“自下而上”。 這種機(jī)制使開發(fā)人員不必管理此類復(fù)雜的依賴關(guān)系圖。
讓我們仔細(xì)看看 @Module()裝飾器。在中 app.module ,我們聲明:
@Module({
controllers: [CatsController],
providers: [CatsService],
})
providers屬性接受一個(gè)提供者數(shù)組。到目前為止,我們已經(jīng)通過一個(gè)類名列表提供了這些提供者。實(shí)際上,該語法providers: [CatsService]是更完整語法的簡(jiǎn)寫:
providers: [
{
provide: CatsService,
useClass: CatsService,
},
];
現(xiàn)在我們看到了這個(gè)顯式的構(gòu)造,我們可以理解注冊(cè)過程。在這里,我們明確地將令牌 CatsService與類 CatsService 關(guān)聯(lián)起來。簡(jiǎn)寫表示法只是為了簡(jiǎn)化最常見的用例,其中令牌用于請(qǐng)求同名類的實(shí)例。
當(dāng)您的要求超出標(biāo)準(zhǔn)提供商所提供的要求時(shí),會(huì)發(fā)生什么?這里有一些例子:
Nest 可讓您定義自定義提供程序來處理這些情況。它提供了幾種定義自定義提供程序的方法。讓我們來看看它們。
useValue 語法對(duì)于注入常量值、將外部庫(kù)放入 Nest 容器或使用模擬對(duì)象替換實(shí)際實(shí)現(xiàn)非常有用。假設(shè)您希望強(qiáng)制 Nest 使用模擬 CatsService 進(jìn)行測(cè)試。
import { CatsService } from './cats.service';
const mockCatsService = {
/* mock implementation
...
*/
};
@Module({
imports: [CatsModule],
providers: [
{
provide: CatsService,
useValue: mockCatsService,
},
],
})
export class AppModule {}
在本例中,CatsService 令牌將解析為 mockCatsService 模擬對(duì)象。useValue 需要一個(gè)值——在本例中是一個(gè)文字對(duì)象,它與要替換的 CatsService 類具有相同的接口。由于 TypeScript 的結(jié)構(gòu)類型化,您可以使用任何具有兼容接口的對(duì)象,包括文本對(duì)象或用 new 實(shí)例化的類實(shí)例。
到目前為止,我們已經(jīng)使用了類名作為我們的提供者標(biāo)記( providers 數(shù)組中列出的提供者中的 Provide 屬性的值)。 這與基于構(gòu)造函數(shù)的注入所使用的標(biāo)準(zhǔn)模式相匹配,其中令牌也是類名。 (如果此概念尚不完全清楚,請(qǐng)參閱DI基礎(chǔ)知識(shí),以重新學(xué)習(xí)令牌)。 有時(shí),我們可能希望靈活使用字符串或符號(hào)作為 DI 令牌。 例如:
import { connection } from './connection';
@Module({
providers: [
{
provide: 'CONNECTION',
useValue: connection,
},
],
})
export class AppModule {}
在本例中,我們將字符串值令牌('CONNECTION')與從外部文件導(dǎo)入的已存在的連接對(duì)象相關(guān)聯(lián)。
除了使用字符串作為令牌之外,還可以使用JavaScript Symbol。
我們前面已經(jīng)看到了如何使用基于標(biāo)準(zhǔn)構(gòu)造函數(shù)的注入模式注入提供者。此模式要求用類名聲明依賴項(xiàng)。'CONNECTION' 自定義提供程序使用字符串值令牌。讓我們看看如何注入這樣的提供者。為此,我們使用 @Inject() 裝飾器。這個(gè)裝飾器只接受一個(gè)參數(shù)——令牌。
@Injectable()
export class CatsRepository {
constructor(@Inject('CONNECTION') connection: Connection) {}
}
@Inject()裝飾器是從@nestjs/common包中導(dǎo)入的。
雖然我們?cè)谏厦娴睦又兄苯邮褂米址?nbsp;'CONNECTION' 來進(jìn)行說明,但是為了清晰的代碼組織,最佳實(shí)踐是在單獨(dú)的文件(例如 constants.ts )中定義標(biāo)記。 對(duì)待它們就像對(duì)待在其自己的文件中定義并在需要時(shí)導(dǎo)入的符號(hào)或枚舉一樣。
useClass語法允許您動(dòng)態(tài)確定令牌應(yīng)解析為的類。 例如,假設(shè)我們有一個(gè)抽象(或默認(rèn))的 ConfigService 類。 根據(jù)當(dāng)前環(huán)境,我們希望 `Nest 提供配置服務(wù)的不同實(shí)現(xiàn)。 以下代碼實(shí)現(xiàn)了這種策略。
const configServiceProvider = {
provide: ConfigService,
useClass:
process.env.NODE_ENV === 'development'
? DevelopmentConfigService
: ProductionConfigService,
};
@Module({
providers: [configServiceProvider],
})
export class AppModule {}
讓我們看一下此代碼示例中的一些細(xì)節(jié)。 您會(huì)注意到,我們首先定義對(duì)象 configServiceProvider,然后將其傳遞給模塊裝飾器的 providers 屬性。 這只是一些代碼組織,但是在功能上等同于我們到目前為止在本章中使用的示例。
另外,我們使用 ConfigService 類名稱作為令牌。 對(duì)于任何依賴 ConfigService 的類,Nest 都會(huì)注入提供的類的實(shí)例( DevelopmentConfigService 或 ProductionConfigService),該實(shí)例將覆蓋在其他地方已聲明的任何默認(rèn)實(shí)現(xiàn)(例如,使用 @Injectable() 裝飾器聲明的 ConfigService)。
useFactory 語法允許動(dòng)態(tài)創(chuàng)建提供程序。實(shí)工廠函數(shù)的返回實(shí)際的 provider 。工廠功能可以根據(jù)需要簡(jiǎn)單或復(fù)雜。一個(gè)簡(jiǎn)單的工廠可能不依賴于任何其他的提供者。更復(fù)雜的工廠可以自己注入它需要的其他提供者來計(jì)算結(jié)果。對(duì)于后一種情況,工廠提供程序語法有一對(duì)相關(guān)的機(jī)制:
下面示例演示:
const connectionFactory = {
provide: 'CONNECTION',
useFactory: (optionsProvider: OptionsProvider) => {
const options = optionsProvider.get();
return new DatabaseConnection(options);
},
inject: [OptionsProvider],
};
@Module({
providers: [connectionFactory],
})
export class AppModule {}
useExisting 語法允許您為現(xiàn)有的提供程序創(chuàng)建別名。這將創(chuàng)建兩種訪問同一提供者的方法。在下面的示例中,(基于string)令牌 'AliasedLoggerService' 是(基于類的)令牌 LoggerService 的別名。假設(shè)我們有兩個(gè)不同的依賴項(xiàng),一個(gè)用于 'AlilasedLoggerService' ,另一個(gè)用于 LoggerService 。如果兩個(gè)依賴項(xiàng)都用單例作用域指定,它們將解析為同一個(gè)實(shí)例。
@Injectable()
class LoggerService {
/* implementation details */
}
const loggerAliasProvider = {
provide: 'AliasedLoggerService',
useExisting: LoggerService,
};
@Module({
providers: [LoggerService, loggerAliasProvider],
})
export class AppModule {}
雖然提供者經(jīng)常提供服務(wù),但他們并不限于這種用途。提供者可以提供任何值。例如,提供程序可以根據(jù)當(dāng)前環(huán)境提供配置對(duì)象數(shù)組,如下所示:
const configFactory = {
provide: 'CONFIG',
useFactory: () => {
return process.env.NODE_ENV === 'development'
? devConfig
: prodConfig;
},
};
@Module({
providers: [configFactory],
})
export class AppModule {}
與任何提供程序一樣,自定義提供程序的作用域僅限于其聲明模塊。要使它對(duì)其他模塊可見,必須導(dǎo)出它。要導(dǎo)出自定義提供程序,我們可以使用其令牌或完整的提供程序?qū)ο蟆?/p>
以下示例顯示了使用 token 的例子:
const connectionFactory = {
provide: 'CONNECTION',
useFactory: (optionsProvider: OptionsProvider) => {
const options = optionsProvider.get();
return new DatabaseConnection(options);
},
inject: [OptionsProvider],
};
@Module({
providers: [connectionFactory],
exports: ['CONNECTION'],
})
export class AppModule {}
但是你也可以使用整個(gè)對(duì)象:
const connectionFactory = {
provide: 'CONNECTION',
useFactory: (optionsProvider: OptionsProvider) => {
const options = optionsProvider.get();
return new DatabaseConnection(options);
},
inject: [OptionsProvider],
};
@Module({
providers: [connectionFactory],
exports: [connectionFactory],
})
export class AppModule {}
更多建議: