NestJS 動態(tài)模塊

2023-09-08 14:33 更新

模塊一章介紹了 Nest 模塊的基礎(chǔ)知識,并簡要介紹了動態(tài)模塊。本章擴(kuò)展了動態(tài)模塊的主題。完成后,您應(yīng)該對它們是什么以及如何以及何時使用它們有很好的了解。

簡介

文檔概述部分中的大多數(shù)應(yīng)用程序代碼示例都使用了常規(guī)或靜態(tài)模塊。模塊定義像提供者和控制器這樣的組件組,它們作為整個應(yīng)用程序的模塊部分組合在一起。它們?yōu)檫@些組件提供了執(zhí)行上下文或范圍。例如,模塊中定義的提供程序?qū)δK的其他成員可見,而不需要導(dǎo)出它們。當(dāng)提供者需要在模塊外部可見時,它首先從其主機(jī)模塊導(dǎo)出,然后導(dǎo)入到其消費(fèi)模塊。

首先,我們將定義一個 UsersModule 來提供和導(dǎo)出 UsersService。UsersModule是 UsersService的主機(jī)模塊。

import { Module } from '@nestjs/common';
import { UsersService } from './users.service';

@Module({
  providers: [UsersService],
  exports: [UsersService],
})
export class UsersModule {}

接下來,我們將定義一個 AuthModule,它導(dǎo)入 UsersModule,使 UsersModule導(dǎo)出的提供程序在 AuthModule中可用:

import { Module } from '@nestjs/common';
import { AuthService } from './auth.service';
import { UsersModule } from '../users/users.module';

@Module({
  imports: [UsersModule],
  providers: [AuthService],
  exports: [AuthService],
})
export class AuthModule {}

這些構(gòu)造使我們能夠注入 UsersService 例如 AuthService 托管在其中的 AuthModule:

import { Injectable } from '@nestjs/common';
import { UsersService } from '../users/users.service';

@Injectable()
export class AuthService {
  constructor(private readonly usersService: UsersService) {}
  /*
    Implementation that makes use of this.usersService
  */
}

我們將其稱為靜態(tài)模塊綁定。Nest在主模塊和消費(fèi)模塊中已經(jīng)聲明了連接模塊所需的所有信息。讓我們來看看這個過程中發(fā)生了什么。Nest通過以下方式使 UsersService 在 AuthModule中可用:

  1. 實(shí)例化 UsersModule ,包括傳遞導(dǎo)入 UsersModule 本身使用的其他模塊,以及傳遞的任何依賴項(xiàng)(參見自定義提供程序)。
  2. 實(shí)例化 AuthModule ,并將 UsersModule 導(dǎo)出的提供者提供給 AuthModule 中的組件(就像在 AuthModule 中聲明它們一樣)。
  3. 在 AuthService 中注入 UsersService 實(shí)例。

動態(tài)模塊實(shí)例

使用靜態(tài)模塊綁定,消費(fèi)模塊不會影響來自主機(jī)模塊的提供者的配置方式。為什么這很重要?考慮這樣一種情況:我們有一個通用模塊,它需要在不同的用例中有不同的行為。這類似于許多系統(tǒng)中的插件概念,在這些系統(tǒng)中,一般功能需要一些配置才能供使用者使用。

Nest 的一個很好的例子是配置模塊。 許多應(yīng)用程序發(fā)現(xiàn)使用配置模塊來外部化配置詳細(xì)信息很有用。 這使得在不同部署中動態(tài)更改應(yīng)用程序設(shè)置變得容易:例如,開發(fā)人員的開發(fā)數(shù)據(jù)庫,測試環(huán)境的數(shù)據(jù)庫等。通過將配置參數(shù)的管理委派給配置模塊,應(yīng)用程序源代碼保持獨(dú)立于配置參數(shù)。

主要在于配置模塊本身,因?yàn)樗峭ㄓ玫?類似于 '插件' ),需要由它的消費(fèi)模塊進(jìn)行定制。這就是動態(tài)模塊發(fā)揮作用的地方。使用動態(tài)模塊特性,我們可以使配置模塊成為動態(tài)的,這樣消費(fèi)模塊就可以使用 API 來控制配置模塊在導(dǎo)入時是如何定制的。

換句話說,動態(tài)模塊提供了一個 API ,用于將一個模塊導(dǎo)入到另一個模塊中,并在導(dǎo)入模塊時定制該模塊的屬性和行為,而不是使用我們目前看到的靜態(tài)綁定。

配置模塊示例

在本節(jié)中,我們將使用示例代碼的基本版本。 截至本章末尾的完整版本在此處可用作工作示例。

我們的要求是使 ConfigModule 接受選項(xiàng)對象以對其進(jìn)行自定義。 這是我們要支持的功能。 基本示例將 .env 文件的位置硬編碼為項(xiàng)目根文件夾。 假設(shè)我們要使它可配置,以便您可以在您選擇的任何文件夾中管理 .env 文件。 例如,假設(shè)您想將各種 .env 文件存儲在項(xiàng)目根目錄下名為 config 的文件夾中(即 src 的同級文件夾)。 在不同項(xiàng)目中使用 ConfigModule 時,您希望能夠選擇其他文件夾。

動態(tài)模塊使我們能夠?qū)?shù)傳遞到要導(dǎo)入的模塊中,以便我們可以更改其行為。 讓我們看看它是如何工作的。 如果我們從最終目標(biāo)開始,即從使用模塊的角度看,然后向后工作,這將很有幫助。 首先,讓我們快速回顧一下靜態(tài)導(dǎo)入 ConfigModule 的示例(即,一種無法影響導(dǎo)入模塊行為的方法)。 請密切注意 @Module() 裝飾器中的 imports 數(shù)組:

import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { ConfigModule } from './config/config.module';

@Module({
  imports: [ConfigModule],
  controllers: [AppController],
  providers: [AppService],
})
export class AppModule {}

讓我們考慮一下動態(tài)模塊導(dǎo)入是什么樣子的,我們在其中傳遞了一個配置對象。比較這兩個例子之間的導(dǎo)入數(shù)組的差異:

import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { ConfigModule } from './config/config.module';

@Module({
  imports: [ConfigModule.register({ folder: './config' })],
  controllers: [AppController],
  providers: [AppService],
})
export class AppModule {}

讓我們看看在上面的動態(tài)示例中發(fā)生了什么。變化的部分是什么?

  1. ConfigModule 是一個普通類,因此我們可以推斷它必須有一個名為 register() 的靜態(tài)方法。我們知道它是靜態(tài)的,因?yàn)槲覀兪窃?nbsp;ConfigModule 類上調(diào)用它,而不是在類的實(shí)例上。注意:我們將很快創(chuàng)建的這個方法可以有任意名稱,但是按照慣例,我們應(yīng)該調(diào)用它 forRoot() 或 register() 方法。
  2. register() 方法是由我們定義的,因此我們可以接受任何我們喜歡的參數(shù)。在本例中,我們將接受具有適當(dāng)屬性的簡單 options 對象,這是典型的情況。
  3. 我們可以推斷 register() 方法必須返回類似模塊的內(nèi)容,因?yàn)樗姆祷刂党霈F(xiàn)在熟悉的導(dǎo)入列表中,到目前為止,我們已經(jīng)看到該列表包含了一個模塊列表。

實(shí)際上,我們的 register() 方法將返回的是 DynamicModule。 動態(tài)模塊無非就是在運(yùn)行時創(chuàng)建的模塊,它具有與靜態(tài)模塊相同屬性,外加一個稱為模塊的附加屬性。 讓我們快速查看一個示例靜態(tài)模塊聲明,并密切注意傳遞給裝飾器的模塊選項(xiàng):

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

動態(tài)模塊必須返回具有完全相同接口的對象,外加一個稱為module的附加屬性。 module屬性用作模塊的名稱,并且應(yīng)與模塊的類名相同,如下例所示。

對于動態(tài)模塊,模塊選項(xiàng)對象的所有屬性都是可選的,模塊除外。

靜態(tài) register() 方法呢? 現(xiàn)在我們可以看到它的工作是返回具有 DynamicModule 接口的對象。 當(dāng)我們調(diào)用它時,我們實(shí)際上是在導(dǎo)入列表中提供一個模塊,類似于在靜態(tài)情況下通過列出模塊類名的方式。 換句話說,動態(tài)模塊 API 只是返回一個模塊,而不是固定 @Modules 裝飾器中的屬性,而是通過編程方式指定它們。

仍然有一些細(xì)節(jié)需要詳細(xì)了解:

  1. 現(xiàn)在我們可以聲明 @Module() 裝飾器的 imports 屬性不僅可以使用一個模塊類名(例如,imports: [UsersModule]) ,還可以使用一個返回動態(tài)模塊的函數(shù)(例如,imports: [ConfigModule.register(...)])。
  2. 動態(tài)模塊本身可以導(dǎo)入其他模塊。 在本示例中,我們不會這樣做,但是如果動態(tài)模塊依賴于其他模塊的提供程序,則可以使用可選的 imports 屬性導(dǎo)入它們。 同樣,這與使用 @Module() 裝飾器為靜態(tài)模塊聲明元數(shù)據(jù)的方式完全相似。

有了這種理解,我們現(xiàn)在可以看看動態(tài) ConfigModule 聲明必須是什么樣子。 讓我們來看一下。

import { DynamicModule, Module } from '@nestjs/common';
import { ConfigService } from './config.service';

@Module({})
export class ConfigModule {
  static register(): DynamicModule {
    return {
      module: ConfigModule,
      providers: [ConfigService],
      exports: [ConfigService],
    };
  }
}

現(xiàn)在應(yīng)該清楚各個部分是如何聯(lián)系在一起的了。調(diào)用 ConfigModule.register(...) 將返回一個 DynamicModule 對象,該對象的屬性基本上與我們通過 @Module() 裝飾器提供的元數(shù)據(jù)相同。

DynamicModule 需要從 @nestjs/common 包導(dǎo)入。

然而,我們的動態(tài)模塊還不是很有趣,因?yàn)槲覀冞€沒有引入任何我們想要配置它的功能。讓我們接下來解決這個問題。

模塊配置

定制 ConfigModule 行為的顯而易見的解決方案是在靜態(tài) register() 方法中向其傳遞一個 options 對象,如我們上面所猜測的。讓我們再次看一下消費(fèi)模塊的 imports 屬性:

import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { ConfigModule } from './config/config.module';

@Module({
  imports: [ConfigModule.register({ folder: './config' })],
  controllers: [AppController],
  providers: [AppService],
})
export class AppModule {}

這很好地處理了將一個 options 對象傳遞給我們的動態(tài)模塊。那么我們?nèi)缭诤?nbsp;ConfigModule 中使用 options 對象呢?讓我們考慮一下。我們知道,我們的 ConfigModule 基本上是一個提供和導(dǎo)出可注入服務(wù)( ConfigService )供其他提供者使用。實(shí)際上我們的 ConfigService 需要讀取 options 對象來定制它的行為?,F(xiàn)在讓我們假設(shè)我們知道如何將 register() 方法中的選項(xiàng)獲取到 ConfigService 中。有了這個假設(shè),我們可以對服務(wù)進(jìn)行一些更改,以便基于 options 對象的屬性自定義其行為。(注意:目前,由于我們還沒有確定如何傳遞它,我們將只硬編碼選項(xiàng)。我們將在一分鐘內(nèi)解決這個問題)。

import { Injectable } from '@nestjs/common';
import * as dotenv from 'dotenv';
import * as fs from 'fs';
import { EnvConfig } from './interfaces';

@Injectable()
export class ConfigService {
  private readonly envConfig: EnvConfig;

  constructor() {
    const options = { folder: './config' };

    const filePath = `${process.env.NODE_ENV || 'development'}.env`;
    const envFile = path.resolve(__dirname, '../../', options.folder, filePath);
    this.envConfig = dotenv.parse(fs.readFileSync(envFile));
  }

  get(key: string): string {
    return this.envConfig[key];
  }
}

現(xiàn)在,我們的 ConfigService 知道如何在選項(xiàng)指定的文件夾中查找 .env 文件。

我們剩下的任務(wù)是以某種方式將 register() 步驟中的 options 對象注入 ConfigService。當(dāng)然,我們將使用依賴注入來做到這一點(diǎn)。這是一個關(guān)鍵點(diǎn),所以一定要理解它。我們的 ConfigModule 提供 ConfigService。而 ConfigService 又依賴于只在運(yùn)行時提供的 options 對象。因此,在運(yùn)行時,我們需要首先將 options 對象綁定到 Nest IoC 容器,然后讓 Nest 將其注入 ConfigService 。請記住,在自定義提供者一章中,提供者可以包含任何值,而不僅僅是服務(wù),所以我們可以使用依賴項(xiàng)注入來處理簡單的 options 對象。

讓我們首先處理將 options 對象綁定到 IoC 容器的問題。我們在靜態(tài) register() 方法中執(zhí)行此操作。請記住,我們正在動態(tài)地構(gòu)造一個模塊,而模塊的一個屬性就是它的提供者列表。因此,我們需要做的是將 options 對象定義為提供程序。這將使它可注入到 ConfigService 中,我們將在下一個步驟中利用它。在下面的代碼中,注意 provider 數(shù)組:

import { DynamicModule, Module } from '@nestjs/common';

import { ConfigService } from './config.service';

@Module({})
export class ConfigModule {
  static register(options): DynamicModule {
    return {
      module: ConfigModule,
      providers: [
        {
          provide: 'CONFIG_OPTIONS',
          useValue: options,
        },
        ConfigService,
      ],
      exports: [ConfigService],
    };
  }
}

現(xiàn)在,我們可以通過將 'CONFIG_OPTIONS' 提供者注入 ConfigService 來完成這個過程?;叵胍幌拢?dāng)我們使用非類令牌定義提供者時,我們需要使用這里描述的 @Inject() 裝飾器。

import { Injectable, Inject } from '@nestjs/common';

import * as dotenv from 'dotenv';
import * as fs from 'fs';

import { EnvConfig } from './interfaces';

@Injectable()
export class ConfigService {
  private readonly envConfig: EnvConfig;

  constructor(@Inject('CONFIG_OPTIONS') private options) {
    const filePath = `${process.env.NODE_ENV || 'development'}.env`;
    const envFile = path.resolve(__dirname, '../../', options.folder, filePath);
    this.envConfig = dotenv.parse(fs.readFileSync(envFile));
  }

  get(key: string): string {
    return this.envConfig[key];
  }
}

最后一點(diǎn):為了簡單起見,我們使用了上面提到的基于字符串的注入標(biāo)記( 'CONFIG_OPTIONS' ),但是最佳實(shí)踐是將它定義為一個單獨(dú)文件中的常量(或符號),然后導(dǎo)入該文件。例如:

export const CONFIG_OPTIONS = 'CONFIG_OPTIONS';


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

掃描二維碼

下載編程獅App

公眾號
微信公眾號

編程獅公眾號