## 動態模塊[#](#dynamic-modules)
[**模塊**](https://docs.nestjs.com/modules)一章介紹了 `Nest` 模塊的基礎知識,并簡要介紹了[動態模塊](https://docs.nestjs.com/modules/modules#dynamic-modules)。本章擴展了動態模塊的主題。完成后,您應該對它們是什么以及如何以及何時使用它們有很好的了解。
### 簡介
文檔概述部分中的大多數應用程序代碼示例都使用了常規或靜態模塊。模塊定義像**提供者**和**控制器**這樣的組件組,它們作為整個應用程序的模塊部分組合在一起。它們為這些組件提供了執行上下文或范圍。例如,模塊中定義的提供程序對模塊的其他成員可見,而不需要導出它們。當提供者需要在模塊外部可見時,它首先從其主機模塊導出,然后導入到其消費模塊。
讓我們來看一個熟悉的例子。
首先,我們將定義一個 `UsersModule` 來提供和導出 `UsersService`。`UsersModule`是 `UsersService`的主機模塊。
```typescript
import { Module } from '@nestjs/common';
import { UsersService } from './users.service';
@Module({
providers: [UsersService],
exports: [UsersService],
})
export class UsersModule {}
```
接下來,我們將定義一個 `AuthModule`,它導入 `UsersModule`,使 `UsersModule`導出的提供程序在 `AuthModule`中可用:
```typescript
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 {}
```
這些構造使我們能夠注入 `UsersService` 例如 `AuthService` 托管在其中的 `AuthModule`:
```typescript
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
*/
}
```
我們將其稱為靜態模塊綁定。`Nest`在主模塊和消費模塊中已經聲明了連接模塊所需的所有信息。讓我們來看看這個過程中發生了什么。`Nest`通過以下方式使 `UsersService` 在 `AuthModule`中可用:
1. 實例化 `UsersModule` ,包括傳遞導入 `UsersModule` 本身使用的其他模塊,以及傳遞的任何依賴項(參見[自定義](8/fundamentals?id=custom-providers)提供程序)。
2. 實例化 `AuthModule` ,并將 `UsersModule` 導出的提供者提供給 `AuthModule` 中的組件(就像在 `AuthModule` 中聲明它們一樣)。
3. 在 `AuthService` 中注入 `UsersService` 實例。
### 動態模塊實例
使用靜態模塊綁定,消費模塊不會影響來自主機模塊的提供者的配置方式。為什么這很重要?考慮這樣一種情況:我們有一個通用模塊,它需要在不同的用例中有不同的行為。這類似于許多系統中的**插件**概念,在這些系統中,一般功能需要一些配置才能供使用者使用。
`Nest` 的一個很好的例子是配置模塊。 許多應用程序發現使用配置模塊來外部化配置詳細信息很有用。 這使得在不同部署中動態更改應用程序設置變得容易:例如,開發人員的開發數據庫,測試環境的數據庫等。通過將配置參數的管理委派給配置模塊,應用程序源代碼保持獨立于配置參數。
主要在于配置模塊本身,因為它是通用的(類似于 `'插件'` ),需要由它的消費模塊進行定制。這就是動態模塊發揮作用的地方。使用動態模塊特性,我們可以使配置模塊成為動態的,這樣消費模塊就可以使用 `API` 來控制配置模塊在導入時是如何定制的。
換句話說,動態模塊提供了一個 `API` ,用于將一個模塊導入到另一個模塊中,并在導入模塊時定制該模塊的屬性和行為,而不是使用我們目前看到的靜態綁定。
### 配置模塊示例
在本節中,我們將使用示例代碼的[基本版本](https://docs.nestjs.com/techniques/configuration#service)。 截至本章末尾的完整版本在[此處](https://github.com/nestjs/nest/tree/master/sample/25-dynamic-modules)可用作工作示例。
我們的要求是使 `ConfigModule` 接受選項對象以對其進行自定義。 這是我們要支持的功能。 基本示例將 `.env` 文件的位置硬編碼為項目根文件夾。 假設我們要使它可配置,以便您可以在您選擇的任何文件夾中管理 `.env` 文件。 例如,假設您想將各種 `.env` 文件存儲在項目根目錄下名為 `config` 的文件夾中(即 `src` 的同級文件夾)。 在不同項目中使用 `ConfigModule` 時,您希望能夠選擇其他文件夾。
動態模塊使我們能夠將參數傳遞到要導入的模塊中,以便我們可以更改其行為。 讓我們看看它是如何工作的。 如果我們從最終目標開始,即從使用模塊的角度看,然后向后工作,這將很有幫助。 首先,讓我們快速回顧一下靜態導入 `ConfigModule` 的示例(即,一種無法影響導入模塊行為的方法)。 請密切注意 `@Module()` 裝飾器中的 `imports` 數組:
```typescript
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 {}
```
讓我們考慮一下動態模塊導入是什么樣子的,我們在其中傳遞了一個配置對象。比較`imports`在這兩個示例在數組中的差異:
```typescript
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 {}
```
讓我們看看在上面的動態示例中發生了什么。變化的部分是什么?
1. `ConfigModule` 是一個普通類,因此我們可以推斷它必須有一個名為 `register()` 的靜態方法。我們知道它是靜態的,因為我們是在 `ConfigModule` 類上調用它,而不是在類的實例上。注意:我們將很快創建的這個方法可以有任意名稱,但是按照慣例,我們應該調用它 `forRoot()` 或 `register()` 方法。
2. `register()` 方法是由我們定義的,因此我們可以接受任何我們喜歡的參數。在本例中,我們將接受具有適當屬性的簡單 `options` 對象,這是典型的情況。
3. 我們可以推斷 `register()` 方法必須返回類似模塊的內容,因為它的返回值出現在熟悉的導入列表中,到目前為止,我們已經看到該列表包含了一個模塊列表。
實際上,我們的 `register()` 方法將返回的是 `DynamicModule`。 動態模塊無非就是在運行時創建的模塊,它具有與靜態模塊相同屬性,外加一個稱為模塊的附加屬性。 讓我們快速查看一個示例靜態模塊聲明,并密切注意傳遞給裝飾器的模塊選項:
```typescript
@Module({
imports: [DogsService],
controllers: [CatsController],
providers: [CatsService],
exports: [CatsService]
})
```
動態模塊必須返回具有完全相同接口的對象,外加一個稱為`module`的附加屬性。 `module`屬性用作模塊的名稱,并且應與模塊的類名相同,如下例所示。
> 對于動態模塊,模塊選項對象的所有屬性都是可選的,模塊除外。
靜態 `register()` 方法呢? 現在我們可以看到它的工作是返回具有 `DynamicModule` 接口的對象。 當我們調用它時,我們實際上是在導入列表中提供一個模塊,類似于在靜態情況下通過列出模塊類名的方式。 換句話說,動態模塊 `API` 只是返回一個模塊,而不是固定 `@Modules` 裝飾器中的屬性,而是通過編程方式指定它們。
仍然有一些細節需要詳細了解:
1. 現在我們可以聲明 `@Module()` 裝飾器的 `imports` 屬性不僅可以使用一個模塊類名(例如,`imports: [UsersModule])` ,還可以使用一個返回動態模塊的函數(例如,`imports: [ConfigModule.register(...)]`)。
2. 動態模塊本身可以導入其他模塊。 在本示例中,我們不會這樣做,但是如果動態模塊依賴于其他模塊的提供程序,則可以使用可選的 `imports` 屬性導入它們。 同樣,這與使用 `@Module()` 裝飾器為靜態模塊聲明元數據的方式完全相似。
有了這種理解,我們現在可以看看動態 `ConfigModule` 聲明必須是什么樣子。 讓我們來看一下。
```typescript
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],
};
}
}
```
現在應該清楚各個部分是如何聯系在一起的了。調用 `ConfigModule.register(...)` 將返回一個 `DynamicModule` 對象,該對象的屬性基本上與我們通過 `@Module()` 裝飾器提供的元數據相同。
> `DynamicModule` 需要從 `@nestjs/common` 包導入。
然而,我們的動態模塊還不是很有趣,因為我們還沒有引入任何我們想要配置它的功能。讓我們接下來解決這個問題。
### 模塊配置
定制 `ConfigModule` 行為的顯而易見的解決方案是在靜態 `register()` 方法中向其傳遞一個 `options` 對象,如我們上面所猜測的。讓我們再次看一下消費模塊的 `imports` 屬性:
```typescript
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` 對象傳遞給我們的動態模塊。那么我們如在何 `ConfigModule` 中使用 `options` 對象呢?讓我們考慮一下。我們知道,我們的 `ConfigModule` 基本上是一個提供和導出可注入服務( `ConfigService` )供其他提供者使用。實際上我們的 `ConfigService` 需要讀取 `options` 對象來定制它的行為。現在讓我們假設我們知道如何將 `register()` 方法中的選項獲取到 `ConfigService` 中。有了這個假設,我們可以對服務進行一些更改,以便基于 `options` 對象的屬性自定義其行為。(注意:目前,由于我們還沒有確定如何傳遞它,我們將只硬編碼選項。我們將在一分鐘內解決這個問題)。
```typescript
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];
}
}
```
現在,我們的 `ConfigService` 知道如何在選項指定的文件夾中查找 `.env` 文件。
我們剩下的任務是以某種方式將 `register()` 步驟中的 `options` 對象注入 `ConfigService`。當然,我們將使用依賴注入來做到這一點。這是一個關鍵點,所以一定要理解它。我們的 `ConfigModule` 提供 `ConfigService`。而 `ConfigService` 又依賴于只在運行時提供的 `options` 對象。因此,在運行時,我們需要首先將 `options` 對象綁定到 `Nest IoC` 容器,然后讓 `Nest` 將其注入 `ConfigService` 。請記住,在**自定義提供者**一章中,提供者可以**包含任何值**,而不僅僅是服務,所以我們可以使用依賴項注入來處理簡單的 `options` 對象。
讓我們首先處理將 `options` 對象綁定到 `IoC` 容器的問題。我們在靜態 `register()` 方法中執行此操作。請記住,我們正在動態地構造一個模塊,而模塊的一個屬性就是它的提供者列表。因此,我們需要做的是將 `options` 對象定義為提供程序。這將使它可注入到 `ConfigService` 中,我們將在下一個步驟中利用它。在下面的代碼中,注意 `provider` 數組:
```typescript
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],
};
}
}
```
現在,我們可以通過將 `'CONFIG_OPTIONS'` 提供者注入 `ConfigService` 來完成這個過程。回想一下,當我們使用非類令牌定義提供者時,我們需要使用[這里](8/fundamentals/custom-providers?id=non-class-based-provider-tokens)描述的 `@Inject()` 裝飾器。
```typescript
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];
}
}
```
最后一點:為了簡單起見,我們使用了上面提到的基于字符串的注入標記( `'CONFIG_OPTIONS'` ),但是最佳實踐是將它定義為一個單獨文件中的常量(或符號),然后導入該文件。例如:
```typescript
export const CONFIG_OPTIONS = 'CONFIG_OPTIONS';
```
### 實例
本章代碼的完整示例可以在[這里](https://github.com/nestjs/nest/tree/master/sample/25-dynamic-modules)找到。
- 介紹
- 概述
- 第一步
- 控制器
- 提供者
- 模塊
- 中間件
- 異常過濾器
- 管道
- 守衛
- 攔截器
- 自定義裝飾器
- 基礎知識
- 自定義提供者
- 異步提供者
- 動態模塊
- 注入作用域
- 循環依賴
- 模塊參考
- 懶加載模塊
- 應用上下文
- 生命周期事件
- 跨平臺
- 測試
- 技術
- 數據庫
- Mongo
- 配置
- 驗證
- 緩存
- 序列化
- 版本控制
- 定時任務
- 隊列
- 日志
- Cookies
- 事件
- 壓縮
- 文件上傳
- 流式處理文件
- HTTP模塊
- Session(會話)
- MVC
- 性能(Fastify)
- 服務器端事件發送
- 安全
- 認證(Authentication)
- 授權(Authorization)
- 加密和散列
- Helmet
- CORS(跨域請求)
- CSRF保護
- 限速
- GraphQL
- 快速開始
- 解析器(resolvers)
- 變更(Mutations)
- 訂閱(Subscriptions)
- 標量(Scalars)
- 指令(directives)
- 接口(Interfaces)
- 聯合類型
- 枚舉(Enums)
- 字段中間件
- 映射類型
- 插件
- 復雜性
- 擴展
- CLI插件
- 生成SDL
- 其他功能
- 聯合服務
- 遷移指南
- Websocket
- 網關
- 異常過濾器
- 管道
- 守衛
- 攔截器
- 適配器
- 微服務
- 概述
- Redis
- MQTT
- NATS
- RabbitMQ
- Kafka
- gRPC
- 自定義傳輸器
- 異常過濾器
- 管道
- 守衛
- 攔截器
- 獨立應用
- Cli
- 概述
- 工作空間
- 庫
- 用法
- 腳本
- Openapi
- 介紹
- 類型和參數
- 操作
- 安全
- 映射類型
- 裝飾器
- CLI插件
- 其他特性
- 遷移指南
- 秘籍
- CRUD 生成器
- 熱重載
- MikroORM
- TypeORM
- Mongoose
- 序列化
- 路由模塊
- Swagger
- 健康檢查
- CQRS
- 文檔
- Prisma
- 靜態服務
- Nest Commander
- 問答
- Serverless
- HTTP 適配器
- 全局路由前綴
- 混合應用
- HTTPS 和多服務器
- 請求生命周期
- 常見錯誤
- 實例
- 遷移指南
- 發現
- 誰在使用Nest?