nest.js 动态模块——迹忆客-ag捕鱼王app官网
模型一章介绍了 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 中可用:
- 实例化 usersmodule,包括传递性地导入 usersmodule 本身使用的其他模块,以及传递性地解决任何依赖关系(请参阅自定义提供程序)。
- 实例化 authmodule,并使 usersmodule 的导出提供者可用于 authmodule 中的组件(就像它们已在 authmodule 中声明一样)。
- 在 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 {}
让我们看看上面的示例中发生了什么。
configmodule
是一个普通的类,所以我们可以推断它肯定有一个静态方法,叫做register()
。 我们知道它是静态的,因为我们在 configmodule 类上调用它,而不是在类的实例上。 注意:我们将很快创建的这个方法可以具有任意名称,但按照惯例,我们应该将其命名为forroot()
或register()
。register()
方法由我们定义,因此我们可以接受任何我们喜欢的输入参数。 在这种情况下,我们将接受一个具有合适属性的简单选项对象,这是典型的情况。- 我们可以推断
register()
方法必须返回类似于模块的东西,因为它的返回值出现在熟悉的imports
列表中,到目前为止我们已经看到包含了一个模块列表。
实际上,我们的 register()
方法会返回一个 dynamicmodule。 动态模块只不过是在运行时创建的模块,具有与静态模块完全相同的属性,加上一个称为 module
的附加属性。 让我们快速回顾一个静态模块声明示例,密切注意传递给装饰器的模块选项:
@module({
imports: [dogsmodule],
controllers: [catscontroller],
providers: [catsservice],
exports: [catsservice]
})
动态模块必须返回一个具有完全相同接口的对象,外加一个名为 module
的附加属性。 module 属性作为模块的名称,应与模块的类名相同,如下例所示。
提示
:对于动态模块,module 选项对象的所有属性都是可选的,除了 module 。
静态 register() 方法呢? 我们现在可以看到它的工作是返回一个具有 dynamicmodule
接口的对象。 当我们调用它时,我们实际上是在为 imports
列表提供一个模块,类似于我们在静态情况下通过列出模块类名来这样做的方式。 换句话说,动态模块 api 只是返回一个模块,而不是修复 @module
装饰器中的属性,我们以编程方式指定它们。
还有一些细节需要介绍:
- 我们现在可以声明 @module() 装饰器的 imports 属性不仅可以采用模块类名称(例如,imports:[usersmodule]),还可以采用返回动态模块的函数(例如,imports:[configmodule.register(. ..)])。
- 动态模块本身可以导入其他模块。 在这个例子中我们不会这样做,但是如果动态模块依赖于其他模块的提供者,你可以使用可选的 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
行为的明显ag捕鱼王app官网的解决方案是在静态 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';