## 动态模块[#](#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?