NestJS 自定義提供者

2023-09-08 14:00 更新

在前面幾章中,我們討論了依賴注入(DI)的各個方面,以及如何在 Nest 中使用它。其中一個例子是基于構造函數(shù)的依賴注入,用于將實例(通常是服務提供者)注入到類中。當您了解到依賴注入是以一種基本的方式構建到 Nest 內(nèi)核中時,您不會感到驚訝。到目前為止,我們只探索了一個主要模式。隨著應用程序變得越來越復雜,您可能需要利用 DI 系統(tǒng)的所有特性,因此讓我們更詳細地研究它們。

依賴注入

依賴注入是一種控制反轉(IoC)技術,您可以將依賴的實例化委派給 IoC 容器(在我們的示例中為 NestJS 運行時系統(tǒng)),而不是必須在自己的代碼中執(zhí)行。 讓我們從“提供者”一章中檢查此示例中發(fā)生的情況。

首先,我們定義一個提供者。@Injectable()裝飾器將 CatsService 類標記為提供者。

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();
  }
}

最后,我們在 Nest IoC 容器中注冊提供程序

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 {}

這個過程有三個關鍵步驟:

1.在 cats.service.ts 中 @Injectable() 裝飾器聲明 CatsService 類是一個可以由Nest IoC容器管理的類。

2.在 cats.controller.ts 中 CatsController 聲明了一個依賴于 CatsService 令牌(token)的構造函數(shù)注入:

constructor(private readonly catsService: CatsService)

3.在 app.module.ts 中,我們將標記 CatsService與 cats.service.ts文件中的 CatsService 類相關聯(lián)。 我們將在下面確切地看到這種關聯(lián)(也稱為注冊)的發(fā)生方式。


當 Nest IoC 容器實例化 CatsController 時,它首先查找所有依賴項*。 當找到 CatsService 依賴項時,它將對 CatsService令牌(token)執(zhí)行查找,并根據(jù)上述步驟(上面的#3)返回 CatsService 類。 假定單例范圍(默認行為),Nest 然后將創(chuàng)建 CatsService 實例,將其緩存并返回,或者如果已經(jīng)緩存,則返回現(xiàn)有實例。

這個解釋稍微簡化了一點。我們忽略的一個重要方面是,分析依賴項代碼的過程非常復雜,并且發(fā)生在應用程序引導期間。一個關鍵特性是依賴關系分析(或“創(chuàng)建依賴關系圖”)是可傳遞的。 在上面的示例中,如果 CatsService 本身具有依賴項,那么那些依賴項也將得到解決。 依賴關系圖確保以正確的順序解決依賴關系-本質(zhì)上是“自下而上”。 這種機制使開發(fā)人員不必管理此類復雜的依賴關系圖。

標準提供者

讓我們仔細看看 @Module()裝飾器。在中 app.module ,我們聲明:

@Module({
  controllers: [CatsController],
  providers: [CatsService],
})

providers屬性接受一個提供者數(shù)組。到目前為止,我們已經(jīng)通過一個類名列表提供了這些提供者。實際上,該語法providers: [CatsService]是更完整語法的簡寫:

providers: [
  {
    provide: CatsService,
    useClass: CatsService,
  },
];

現(xiàn)在我們看到了這個顯式的構造,我們可以理解注冊過程。在這里,我們明確地將令牌 CatsService與類 CatsService 關聯(lián)起來。簡寫表示法只是為了簡化最常見的用例,其中令牌用于請求同名類的實例。

自定義提供者

當您的要求超出標準提供商所提供的要求時,會發(fā)生什么?這里有一些例子:

  • 您要創(chuàng)建自定義實例,而不是讓 Nest 實例化(或返回其緩存實例)類
  • 您想在第二個依賴項中重用現(xiàn)有的類
  • 您想使用模擬版本覆蓋類進行測試

Nest 可讓您定義自定義提供程序來處理這些情況。它提供了幾種定義自定義提供程序的方法。讓我們來看看它們。

值提供者 (useValue)

useValue 語法對于注入常量值、將外部庫放入 Nest 容器或使用模擬對象替換實際實現(xiàn)非常有用。假設您希望強制 Nest 使用模擬 CatsService 進行測試。

import { CatsService } from './cats.service';

const mockCatsService = {
  /* mock implementation
  ...
  */
};

@Module({
  imports: [CatsModule],
  providers: [
    {
      provide: CatsService,
      useValue: mockCatsService,
    },
  ],
})
export class AppModule {}

在本例中,CatsService 令牌將解析為 mockCatsService 模擬對象。useValue 需要一個值——在本例中是一個文字對象,它與要替換的 CatsService 類具有相同的接口。由于 TypeScript 的結構類型化,您可以使用任何具有兼容接口的對象,包括文本對象或用 new 實例化的類實例。

非類提供者

到目前為止,我們已經(jīng)使用了類名作為我們的提供者標記( providers 數(shù)組中列出的提供者中的 Provide 屬性的值)。 這與基于構造函數(shù)的注入所使用的標準模式相匹配,其中令牌也是類名。 (如果此概念尚不完全清楚,請參閱DI基礎知識,以重新學習令牌)。 有時,我們可能希望靈活使用字符串或符號作為 DI 令牌。 例如:

import { connection } from './connection';

@Module({
  providers: [
    {
      provide: 'CONNECTION',
      useValue: connection,
    },
  ],
})
export class AppModule {}

在本例中,我們將字符串值令牌('CONNECTION')與從外部文件導入的已存在的連接對象相關聯(lián)。

除了使用字符串作為令牌之外,還可以使用JavaScript Symbol

我們前面已經(jīng)看到了如何使用基于標準構造函數(shù)的注入模式注入提供者。此模式要求用類名聲明依賴項。'CONNECTION' 自定義提供程序使用字符串值令牌。讓我們看看如何注入這樣的提供者。為此,我們使用 @Inject() 裝飾器。這個裝飾器只接受一個參數(shù)——令牌。

@Injectable()
export class CatsRepository {
  constructor(@Inject('CONNECTION') connection: Connection) {}
}

@Inject()裝飾器是從@nestjs/common包中導入的。

雖然我們在上面的例子中直接使用字符串 'CONNECTION' 來進行說明,但是為了清晰的代碼組織,最佳實踐是在單獨的文件(例如 constants.ts )中定義標記。 對待它們就像對待在其自己的文件中定義并在需要時導入的符號或枚舉一樣。

類提供者 (useClass)

useClass語法允許您動態(tài)確定令牌應解析為的類。 例如,假設我們有一個抽象(或默認)的 ConfigService 類。 根據(jù)當前環(huán)境,我們希望 `Nest 提供配置服務的不同實現(xiàn)。 以下代碼實現(xiàn)了這種策略。

const configServiceProvider = {
  provide: ConfigService,
  useClass:
    process.env.NODE_ENV === 'development'
      ? DevelopmentConfigService
      : ProductionConfigService,
};

@Module({
  providers: [configServiceProvider],
})
export class AppModule {}

讓我們看一下此代碼示例中的一些細節(jié)。 您會注意到,我們首先定義對象 configServiceProvider,然后將其傳遞給模塊裝飾器的 providers 屬性。 這只是一些代碼組織,但是在功能上等同于我們到目前為止在本章中使用的示例。

另外,我們使用 ConfigService 類名稱作為令牌。 對于任何依賴 ConfigService 的類,Nest 都會注入提供的類的實例( DevelopmentConfigService 或 ProductionConfigService),該實例將覆蓋在其他地方已聲明的任何默認實現(xiàn)(例如,使用 @Injectable() 裝飾器聲明的 ConfigService)。

工廠提供者 (useFactory)

useFactory 語法允許動態(tài)創(chuàng)建提供程序。實工廠函數(shù)的返回實際的 provider 。工廠功能可以根據(jù)需要簡單或復雜。一個簡單的工廠可能不依賴于任何其他的提供者。更復雜的工廠可以自己注入它需要的其他提供者來計算結果。對于后一種情況,工廠提供程序語法有一對相關的機制:

  1. 工廠函數(shù)可以接受(可選)參數(shù)。
  2. inject 屬性接受一個提供者數(shù)組,在實例化過程中,Nest 將解析該數(shù)組并將其作為參數(shù)傳遞給工廠函數(shù)。這兩個列表應該是相關的: Nest 將從 inject 列表中以相同的順序將實例作為參數(shù)傳遞給工廠函數(shù)。

下面示例演示:

const connectionFactory = {
  provide: 'CONNECTION',
  useFactory: (optionsProvider: OptionsProvider) => {
    const options = optionsProvider.get();
    return new DatabaseConnection(options);
  },
  inject: [OptionsProvider],
};

@Module({
  providers: [connectionFactory],
})
export class AppModule {}

別名提供者 (useExisting)

useExisting 語法允許您為現(xiàn)有的提供程序創(chuàng)建別名。這將創(chuàng)建兩種訪問同一提供者的方法。在下面的示例中,(基于string)令牌 'AliasedLoggerService' 是(基于類的)令牌 LoggerService 的別名。假設我們有兩個不同的依賴項,一個用于 'AlilasedLoggerService' ,另一個用于 LoggerService 。如果兩個依賴項都用單例作用域指定,它們將解析為同一個實例。

@Injectable()
class LoggerService {
  /* implementation details */
}

const loggerAliasProvider = {
  provide: 'AliasedLoggerService',
  useExisting: LoggerService,
};

@Module({
  providers: [LoggerService, loggerAliasProvider],
})
export class AppModule {}

非服務提供者

雖然提供者經(jīng)常提供服務,但他們并不限于這種用途。提供者可以提供任何值。例如,提供程序可以根據(jù)當前環(huán)境提供配置對象數(shù)組,如下所示:

const configFactory = {
  provide: 'CONFIG',
  useFactory: () => {
    return process.env.NODE_ENV === 'development'
      ? devConfig
      : prodConfig;
  },
};

@Module({
  providers: [configFactory],
})
export class AppModule {}

導出自定義提供者

與任何提供程序一樣,自定義提供程序的作用域僅限于其聲明模塊。要使它對其他模塊可見,必須導出它。要導出自定義提供程序,我們可以使用其令牌或完整的提供程序對象。

以下示例顯示了使用 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 {}

但是你也可以使用整個對象:

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 {}


以上內(nèi)容是否對您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號
微信公眾號

編程獅公眾號