Nest.js 动态模块

模型一章介绍了 Nest 模型的基础知识,并简要介绍了动态模型。 本章对动态模型的进行扩展。 完成后,我们应该很好地掌握它们是什么以及何时并且如何使用它们。

介绍

文档概述部分中的大多数应用程序代码示例都使用常规或静态模块。 模块定义了组件组,例如提供者和控制器,它们作为整个应用程序的模块化部分组合在一起。 它们为这些组件提供执行上下文或范围。 例如,模块中定义的提供程序对模块的其他成员可见,而无需导出它们。 当一个提供者需要在模块之外可见时,它首先从它的宿主模块中导出,然后导入到它的消费模块中。

让我们来看一个熟悉的例子。

首先,我们将定义一个 UsersModule 来提供和导出一个 UsersService。 UsersModule 是 UsersService 的宿主模块。

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

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

接下来,我们将定义一个 AuthModule,它导入 UsersModule,使 UsersModule 的导出提供者在 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 {}

这些构造允许我们将 UsersService 注入到,例如 AuthModule 中托管的 AuthService 中:

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

@Injectable()
export class AuthService {
  constructor(private usersService: UsersService) {}
  /*
    使用 this.usersService 的实现
  */
}

我们将其称为静态模块绑定。 Nest 需要将模块连接在一起的所有信息都已在主机和消费模块中声明。 让我们分析一下这个过程中发生的事情。 Nest 通过以下方式使用户服务在 AuthModule 中可用:

  1. 实例化 UsersModule,包括传递性地导入 UsersModule 本身使用的其他模块,以及传递性地解决任何依赖关系(请参阅自定义提供程序)。
  2. 实例化 AuthModule,并使 UsersModule 的导出提供者可用于 AuthModule 中的组件(就像它们已在 AuthModule 中声明一样)。
  3. 在 AuthService 中注入 UsersService 的实例。

动态模型用例

使用静态模块绑定,消费模块没有机会影响主机模块提供者的配置方式。为什么这很重要?考虑我们有一个通用模块需要在不同用例中表现不同的情况。这类似于许多系统中的“插件”概念,其中通用设施需要一些配置才能被消费者使用。

Nest 的一个很好的例子是配置模块。许多应用程序发现使用配置模块将配置细节外部化很有用。这使得在不同部署中动态更改应用程序设置变得容易:例如,开发人员的开发数据库,暂存/测试环境的暂存数据库等。通过将配置参数的管理委托给配置模块,应用程序源代码独立于配置参数。

挑战在于配置模块本身,因为它是通用的(类似于“插件”),需要通过其消费模块进行定制。这就是动态模块发挥作用的地方。使用动态模块特性,我们可以使我们的配置模块动态化,以便使用模块可以使用 API 来控制配置模块在导入时如何定制。

换句话说,动态模块提供了一个 API,用于将一个模块导入另一个模块,并在导入该模块时自定义该模块的属性和行为,而不是使用我们目前看到的静态绑定。


配置模块示例

本章末尾可获得示例的完整代码。

我们的要求是让 ConfigModule 接受一个 options 对象来自定义它。这是我们想要支持的功能。基本示例将 .env 文件的位置硬编码到项目根文件夹中。假设我们想要使其可配置,这样我们就可以在选择的任何文件夹中管理自己的 .env 文件。例如,假设我们想将各种 .env 文件存储在项目根目录下名为 config 的文件夹中(即 src 的同级文件夹)。在不同项目中使用 ConfigModule 时,我们希望能够选择不同的文件夹。

动态模块使我们能够将参数传递给正在导入的模块,以便我们可以更改其行为。让我们看看这是如何工作的。如果我们从消费模块的角度来看这可能看起来如何的最终目标开始,然后向后执行,这将很有帮助。首先,让我们快速回顾一下静态导入 ConfigModule 的示例(即,一种无法影响导入模块行为的方法)。密切注意 @Module() 装饰器中的 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],
  controllers: [AppController],
  providers: [AppService],
})
export class AppModule {}

让我们考虑一下动态模块导入(我们在其中传递配置对象)可能是什么样子。 比较这两个示例之间 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 {}

让我们看看上面的示例中发生了什么。

  1. ConfigModule 是一个普通的类,所以我们可以推断它肯定有一个静态方法,叫做 register()。 我们知道它是静态的,因为我们在 ConfigModule 类上调用它,而不是在类的实例上。 注意:我们将很快创建的这个方法可以具有任意名称,但按照惯例,我们应该将其命名为 forRoot()register()
  2. register() 方法由我们定义,因此我们可以接受任何我们喜欢的输入参数。 在这种情况下,我们将接受一个具有合适属性的简单选项对象,这是典型的情况。
  3. 我们可以推断 register() 方法必须返回类似于模块的东西,因为它的返回值出现在熟悉的 imports 列表中,到目前为止我们已经看到包含了一个模块列表。

实际上,我们的 register() 方法会返回一个 DynamicModule。 动态模块只不过是在运行时创建的模块,具有与静态模块完全相同的属性,加上一个称为 module 的附加属性。 让我们快速回顾一个静态模块声明示例,密切注意传递给装饰器的模块选项:

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

动态模块必须返回一个具有完全相同接口的对象,外加一个名为 module 的附加属性。 module 属性作为模块的名称,应与模块的类名相同,如下例所示。

提示 :对于动态模块,module 选项对象的所有属性都是可选的,除了 module 。

静态 register() 方法呢? 我们现在可以看到它的工作是返回一个具有 DynamicModule 接口的对象。 当我们调用它时,我们实际上是在为 imports 列表提供一个模块,类似于我们在静态情况下通过列出模块类名来这样做的方式。 换句话说,动态模块 API 只是返回一个模块,而不是修复 @Module 装饰器中的属性,我们以编程方式指定它们。

还有一些细节需要介绍:

  1. 我们现在可以声明 @Module() 装饰器的 imports 属性不仅可以采用模块类名称(例如,imports:[UsersModule]),还可以采用返回动态模块的函数(例如,imports:[ConfigModule.register(. ..)])。
  2. 动态模块本身可以导入其他模块。 在这个例子中我们不会这样做,但是如果动态模块依赖于其他模块的提供者,你可以使用可选的 imports 属性来导入它们。 同样,这与使用 @Module() 装饰器为静态模块声明元数据的方式完全相同。

有了这些理解,我们现在可以看看我们的动态 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],
    };
  }
}

现在应该清楚这些部分是如何联系在一起的。 调用 ConfigModule.register(...) 返回一个 DynamicModule 对象,其属性与迄今为止我们通过 @Module() 装饰器作为元数据提供的属性基本相同。

提示 : 从@nestjs/common 导入 DynamicModule。


模块配置

自定义 ConfigModule 行为的明显解决方案是在静态 register() 方法中传递一个 options 对象,正如我们上面所猜测的。 让我们再看看我们的消费模块的 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 对象传递给我们的动态模块。 那么我们如何在 ConfigModule 中使用该对象呢? 让我们考虑一下。 我们知道我们的 ConfigModule 基本上是一个提供和导出可注入服务的主机 - ConfigService - 供其他提供者使用。 实际上是我们的 ConfigService 需要读取 options 对象来自定义其行为。 让我们暂时假设我们知道如何以某种方式从 register() 方法中获取选项到 ConfigService 中。 有了这个假设,我们可以对服务进行一些更改,从而根据 options 对象的属性自定义其行为。 (注意:目前,由于我们还没有真正确定如何传递它,我们只是硬编码选项。我们会在一分钟内解决这个问题)。

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 知道如何在我们在 options 中指定的文件夹中找到 .env 文件。

我们剩下的任务是以某种方式将 register() 步骤中的 options 对象注入到我们的 ConfigService 中。当然,我们将使用依赖注入来做到这一点。这是一个关键点,所以请确保已经理解了它。我们的 ConfigModule 提供 ConfigService。 ConfigService 又依赖于仅在运行时提供的 options 对象。因此,在运行时,我们需要首先将 options 对象绑定到 Nest IoC 容器,然后让 Nest 将其注入到我们的 ConfigService 中。请记住,在自定义提供者一章中,提供者可以包含任何值,而不仅仅是服务,因此我们可以使用依赖注入来处理简单的 options 对象。

让我们首先解决将 options 对象绑定到 IoC 容器的问题。我们在静态 register() 方法中执行此操作。请记住,我们正在动态构建模块,模块的属性之一是它的 providers 列表。所以我们需要做的就是将我们的 options 对象定义为提供者。这将使它可以注入到 ConfigService 中,我们将在下一步中利用它。在下面的代码中,注意 providers 数组:

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 来完成该过程。 回想一下,当我们使用非类令牌定义提供者时,我们需要使用这里描述的 @Inject() 装饰器。

import * as dotenv from 'dotenv';
import * as fs from 'fs';
import { Injectable, Inject } from '@nestjs/common';
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'),但最佳实践是将其定义为单独文件中的常量(或符号),然后导入该文件。 例如:

export const CONFIG_OPTIONS = 'CONFIG_OPTIONS';

完整示例

https://github.com/nestjs/nest/tree/master/sample/25-dynamic-modules

查看笔记

扫码一下
查看教程更方便