模塊一章介紹了 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中可用:
使用靜態(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ā)生了什么。變化的部分是什么?
實(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ì)了解:
有了這種理解,我們現(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';
更多建議: