自定义提供者
在早期的章节中,我们涉及了**依赖注入(DI)**的各个方面,以及它在 Nest 中的使用方式。 其中一个例子是使用构造函数注入将实例(通常是服务提供者)注入到类中。 您可能不会感到惊讶地了解到,依赖注入在 Nest 核心中以一种基本的方式内置。 到目前为止,我们只探讨了一个主要的模式。随着应用程序变得更加复杂,您可能需要充分利用 DI 系统的全部功能, 因此让我们更详细地探讨它们。
依赖注入基础知识
依赖注入是一种**控制反转(IoC)**技术,其中您将依赖项的实例化委托给IoC容器 (在我们的情况下是 NestJS 运行时系统),而不是在您自己的代码中以命令式的方式执行。 让我们来看一下来自提供者章节的这个例子中正在发生什么。
首先,我们定义了一个提供者。@Injectable() 装饰器将 CatsService 类标记为提供者。
import { Injectable } from '@nestjs/common';
import { Cat } from './interfaces/cat.interface';
@Injectable()
export class CatsService {
private readonly cats: Cat[] = [];
findAll(): Cat[] {
return this.cats;
}
}
然后,我们请求Nest将提供者程序注入到我们的控制器类中:
import { Controller, Get } from '@nestjs/common';
import { CatsService } from './cats.service';
import { Cat } from './interfaces/cat.interface';
@Controller('cats')
export class CatsController {
constructor(private catsService: CatsService) {}
@Get()
async findAll(): Promise<Cat[]> {
return this.catsService.findAll();
}
}
最后,我们向Nest IoC容器注册提供者程序:
import { Module } from '@nestjs/common';
import { CatsController } from './cats/cats.controller';
import { CatsService } from './cats/cats.service';
@Module({
controllers: [CatsController],
providers: [CatsService],
})
export class AppModule {}
究竟是什么在暗中发生,使得这一切得以实现?这个过程有三个关键步骤:
- 在
cats.service.ts中,我们使用@Injectable()装饰器将CatsService类声明为可由Nest IoC容器管理的类。 - 在
cats.controller.ts中,CatsController通过构造器注入声明了对CatsService标记的依赖。constructor(private catsService: CatsService) {} - 在
app.module.ts中, 我们将字符CatsService与cats.service.ts文件中的CatsService类关联起来。
我们将在下面详细了解这种关联(也称为注册)是如何发生的。
当Nest IoC容器实例化CatsController时,它首先查找任何依赖项。
当它找到CatsService依赖项时,
它会在CatsService标记上执行查找,该标记返回CatsService类,
根据注册步骤(上述的#3)。假设是SINGLETON范围(默认行为),Nest将创建一个CatsService实例,
将其缓存并返回,或者如果已经缓存,则返回现有实例。
这个解释有点简化以阐明观点。我们忽略的一个重要领域是分析代码以查找依赖项的过程非常复杂,并且发生在应用程序启动期间。 一个关键特征是依赖性分析(或“创建依赖图”)是传递的。 在上述示例中,如果CatsService本身有依赖关系,这些依赖关系也将被解决。 依赖图确保以正确的顺序解决依赖关系 - 本质上是“自底向上”。 这种机制使开发人员无需管理如此复杂的依赖关系图。
标准提供者
让我们仔细看看@Module()装饰器。在app.module中,我们声明:
@Module({
controllers: [CatsController],
providers: [CatsService],
})
providers属性采用一组提供者。
到目前为止,我们已经通过类名列表提供了这些提供者程序。
事实上,语法providers: [CatsService]是更完整语法的简写形式:
providers: [
{
provide: CatsService,
useClass: CatsService,
},
]
现在我们看到了这个显式结构,就能理解注册过程了。
在这里,我们明确地将CatsService标记(token)与CatsService类关联起来。
这种简称只是为了简化最常见的使用情况,即使用标记来请求同名类的实例。
自定义提供程序
当你的需求超出标准提供程序提供的范围时会发生什么呢?以下是一些例子:
- 你想要创建一个自定义实例,而不是让Nest实例化(或返回缓存的实例)一个类
- 你想要在第二个依赖中重用一个现有类
- 你想要用测试的模拟版本覆盖一个类 Nest允许你定义自定义提供程序来处理这些情况。它提供了几种定义自定义提供程序的方式。 让我们逐步了解它们。
如果你在依赖项解析方面遇到问题,你可以设置NEST_DEBUG环境变量,在启动期间获得额外的依赖项解析日志。
Value提供程序: useValue
useValue语法对于注入常量值、将外部库 放入Nest容器中或用模拟对象替换真实实现非常有用。
比如说,你想要强制Nest在测试目的中使用一个模拟的CatsService。
import { CatsService } from './cats.service';
const mockCatsService = {
/* mock implementation */
};
@Module({
imports: [CatsModule],
providers: [
{
provide: CatsService,
useValue: mockCatsService,
},
],
})
export class AppModule {}
在这个例子中,CatsService标记将解析为mockCatsService模拟对象。
useValue需要一个值 - 在这种情况下,它是一个字面对象,它具有与它替代的CatsService类相同的接口。
由于TypeScript的结构类型,你可以使用任何具有兼容接口的对象,包括字面对象或使用new实例化的类实例。
非基于类的提供程序标记
到目前为止,我们一直使用类名作为我们的提供程序标记(在providers数组中列出的提供程序的provide属性的值)。
这与基于构造函数注入中使用的标准模式相匹配,
其中标记也是类名(如果这个概念不太清晰,请参考DI基础知识进行复习)。
有时,我们可能希望灵活地使用字符串或符号作为DI标记。例如:
import { connection } from './connection';
@Module({
providers: [
{
provide: 'CONNECTION',
useValue: connection,
},
],
})
export class AppModule {}
在这个例子中,我们将一个字符串值的标记('CONNECTION')与我们从外部文件导入的预先存在的连接对象关联起来。
除了使用字符串作为标记值外,你还可以使用JavaScript符号或TypeScript枚举。
我们之前已经看到了如何使用标准的基于构造函数注入模式注入提供程序。
这种模式要求依赖关系使用类名声明。'CONNECTION'自定义提供程序使用字符串值的标记。
让我们看看如何注入这样的提供程序。为此,我们使用@Inject()装饰器。这个装饰器接受一个参数 - 标记。
@Injectable()
export class CatsRepository {
constructor(@Inject('CONNECTION') connection: Connection) {}
}
@Inject()装饰器从@nestjs/common包中导入。
虽然在上面的例子中我们直接使用字符串'CONNECTION'用于说明目的,但为了代码组织的清晰性,
最好的实践是在一个单独的文件中定义标记,比如constants.ts。
将它们视为在自己的文件中定义并在需要 的地方导入的符号或枚举。
类提供程序: useClass
useClass语法允许你动态确定一个标记应该解析为哪个类。
例如,假设我们有一个抽象(或默认)的ConfigService类。
根据当前的环境,我们希望Nest提供不同的配置服务实现。
以下代码实现了这样的策略。
const configServiceProvider = {
provide: ConfigService,
useClass:
process.env.NODE_ENV === 'development'
? DevelopmentConfigService
: ProductionConfigService,
};
@Module({
providers: [configServiceProvider],
})
export class AppModule {}
让我们在这个代码示例中看一些细节。
你会注意到我们首先用一个字面对象定义了configServiceProvider,
然后将它传递给模块装饰器的providers属性。
这只是 一点代码组织,但在功能上等同于本章迄今为止使用的示例。
此外,我们已经将ConfigService类名用作我们的标记。
对于任何依赖于ConfigService的类,Nest将注入由提供的类的实例
(DevelopmentConfigService或ProductionConfigService),
覆盖可能在其他地方声明的任何默认实现(例如,使用@Injectable()装饰器声明的ConfigService)。
工厂提供程序: useFactory
useFactory 语法允许动态创建提供程序。 实际的提供程序将由从工厂函数返回的值提供。 工厂函数可以根据需要简单或复杂。 简单的工厂可能不依赖于任何其他提供程序。 更复杂的工厂可以自身注入其需要计算结果的其他提供程序。 对于后者,工厂提供程序语法具有一对相关的机制:
- 工厂函数可以接受(可选的)参数。
- (可选的)inject 属性接受一个由Nest解析并在实例化过程中作为参数传递给工厂函数的提供程序数组。
此外,这些提供程序可以被标记为可选的。
这两个列表应该是相关的:Nest将以相同的顺序将来自 inject 列表的实例作为参数传递给工厂函数。
下面的例子演示了这一点。
const connectionProvider = {
provide: 'CONNECTION',
useFactory: (optionsProvider: OptionsProvider, optionalProvider?: string) => {
const options = optionsProvider.get();
return new DatabaseConnection(options);
},
inject: [OptionsProvider, { token: 'SomeOptionalProvider', optional: true }],
// \_____________/ \__________________/
// This provider The provider with this
// is mandatory. token can resolve to `undefined`.
};
@Module({
providers: [
connectionProvider,
OptionsProvider,
// { provide: 'SomeOptionalProvider', useValue: 'anything' },
],
})
export class AppModule {}
别名提供程序: useExisting
useExisting语法允许你为现有的提供程序创建别名。
这样可以创建两种访问相同提供程序的方式。
在下面的例子中,(基于字符串的)标记'AliasedLoggerService'是(基于类的)标记LoggerService 的别名。
假设我们有两个不同的依赖项,一个用于'AliasedLoggerService',另一个用于LoggerService。
如果两个依赖项都使用SINGLETON范围指定,它们将都解析为相同的实例。
@Injectable()
class LoggerService { /* ... */ }
const loggerAliasProvider = {
provide: 'AliasedLoggerService',
useExisting: LoggerService,
};
@Module({
providers: [LoggerService, loggerAliasProvider],
})
export class AppModule {}
非基于服务的提供程序
虽然提供者程序通常提供服务,但它们并不局限于此用法。 提供者程序可以提供任何值。例如,一个提供程序可以根据当前环境提供一个配置对象数组,如下所示:
const configFactory = {
provide: 'CONFIG',
useFactory: () => {
return process.env.NODE_ENV === 'development' ? devConfig : prodConfig;
}
}
@Module({
providers: [configFactory],
})
export class AppModule {}
导出自定义提供者程序
与任何提供者程序一样,自定义提供者程序的范围仅限于其声明模块。 为了使其对其他模块可见,必须将其导出。 要导出自定义提供者程序,我们可以使用其标记(token)或完整的提供者程序对象。
以下示例显示使用标记(token)导出:
const connectionFactory = {
provide: 'CONNECTION',
useFactory: (optionsProvider: OptionsProvider) => {
const options = optionsProvider.get();
return new DatabaseConnection(options);
},
inject: [OptionsProvider],
}
@Module({
providers: [connectionFactory],
exports: ['CONNECTION'],
})
export class AppModule {}
或者,使用完整的提供者程序对象导出:
const connectionFactory = {
provide: 'CONNECTION',
useFactory: (optionsProvider: OptionsProvider) => {
const options = optionsProvider.get();
return new DatabaseConnection(options);
},
inject: [OptionsProvider],
}
@Module({
providers: [connectionFactory],
exports: [connectionFactory],
})
export class AppModule {}
异步提供程序
有时,在一个或多个异步任务完成之前,应用程序启动应该被延迟。 例如,你可能不希望在与数据库的连接建立之前开始接受请求。 你可以使用异步提供程序来实现这一点。
这种情况下的语法是在useFactory 语法中使用async/await。
工厂返回一个Promise,工厂函数可以等待(await)异步任务。
Nest将在实例化依赖(注入)这样的提供程序的任何类之前等待Promise的解析。
{
provide: 'ASYNC_CONNECTION',
useFactory: async () => {
const connection = await createConnection(options);
return connection;
},
}
在这里学习更多自定义提供者语法
注入
与任何其他提供者程序一样,异步提供者程序通过其标记(token)注入到其他组件。
在上面的示例中,您将使用构造函数@Inject('ASYNC_CONNECTION')。
动态模块
在《模块》章节中,介绍了 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 {}
这些构造允许我们在AuthModule中注入UsersService,
例如,AuthService托管在AuthModule中:
import { Injectable } from '@nestjs/common';
import { UsersService } from '../users/users.service';
@Injectable()
export class AuthService {
constructor(private usersService: UsersService) {}
/*
利用 this.usersService 的实现
*/
}
我们将这称为静态模块绑定。 Nest 需要将模块连接在一起的所有信息都已在宿主模块和使用模块中声明。 让我们解开此过程中发生的事情。
Nest 通过以下步骤在AuthModule中使UsersService可用:
- 实例化
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。
静态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(...)返回一个具有与我们迄今为止
通过@Module() 装饰器提供的元数据完全相同的属性的DynamicModule对象。
从 @nestjs/common 导入 DynamicModule。
然而,我们的动态模块还不够有趣,因为我们还没有引入任何配置它的功能,而我们之前说过我们想要这样做。 让我们接下来解决这个问题。
模块配置
定制ConfigModule行为的明显解决方案是在静态register()方法中传递一个选项对象,
就像我们上面猜测的那样。让我们再次看一下我们消费模块的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中使用该options对象呢?让我们思考一下。
我们知道我们的ConfigModule基本上是一个提供和导出可注入服务ConfigService的主机,供其他提供者使用。
实际上,我们的ConfigService需要读取options对象以自定义其行为。
暂时假设我们知道如何以某种方式将options从register()方法传递到ConfigService。有了这个假设,
我们可以对服务进行一些更改,根据options对象中的属性自定义其行为。
(注意:当前我们还没有确定如何传递它,所以我们将选项硬编码。我们一会儿就会解决这个问题。)
import { Injectable } from '@nestjs/common';
import * as dotenv from 'dotenv';
import * as fs from 'fs';
import * as path from 'path';
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()步骤注入到ConfigService中。
当然,我们将使用依赖注入来完成这个任务。这是一个关键点,所以确保您理解它。
我们的ConfigModule提供ConfigService。
ConfigService反过来依赖于仅在运行时提供的选项对象。
因此,在运行时,
我们首先需要将选项对象绑定到Nest IoC 容器,
然后让Nest将其注入到我们的ConfigService中。
请记住在自定义提供者章节中提到,提供者可以包含任何值,不仅仅是服务,
因此我们可以使用依赖注入来处理一个简单的选项对象。
首先让我们解决将选项对象绑定到 IoC 容器的问题。
我们在静态register()方法中完成这项工作。
记住我们正在动态构建一个模块,模块的属性之一是其提供者列表。
因此,我们需要将我们的选项对象定义为提供者。
这将使其成为可注入到ConfigService中的对象,我们将在下一步中利用这一点。
在下面的代码中,注意providers数组:
import { DynamicModule, Module } from '@nestjs/common';
import { ConfigService } from './config.service';
@Module({})
export class ConfigModule {
static register(options: Record<string, any>): 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 * as path from 'path';
import { Injectable, Inject } from '@nestjs/common';
import { EnvConfig } from './interfaces';
@Injectable()
export class ConfigService {
private readonly envConfig: EnvConfig;
constructor(@Inject('CONFIG_OPTIONS') private options: Record<string, any>) {
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';
示例
本章中代码的完整示例可以在这里找到。
社区指南
您可能已经看到了一些@nestjs/包中使用forRoot、register和forFeature等方法的用法,
并且可能想知道所有这些方法的区别是什么。关于这个没有硬性规定,但@nestjs/包尝试遵循这些指南:
-
使用
register创建模块时,您希望使用特定配置来配置仅由调用模块使用的动态模块。 例如,在 Nest 的@nestjs/axios中:HttpModule.register({ baseUrl: 'someUrl' })。 如果在另一个模块中使用HttpModule.register({ baseUrl: 'somewhere else' }), 它将具有不同的配置。您可以为所需数量的模块执行此操作。 -
使用
forRoot创建模块时,您希望配置一个动态模块一次,并在多个位置重用该配置 (尽管可能并不知道,因为它被抽象掉了)。 这就是为什么您有一个GraphQLModule.forRoot(),一个TypeOrmModule.forRoot()等的原因。 -
使用
forFeature创建模块时,您希望使用动态模块的forRoot配置, 但需要修改一些特定于调用模块需求的配置(即这个模块应该访问哪个存储库,或者记录器应该使用的上下文)。
通常,所有这些方法都有它们的async对应物,比如registerAsync、forRootAsync和forFeatureAsync,
它们的含义相同,但也使用 Nest 的依赖注入来进行配置。
可配置模块构建器
手动创建高度可配置的动态模块并公开async方法(如 registerAsync、forRootAsync 等)
相当复杂,尤其对于初学者来说。因此,Nest 提供了 ConfigurableModuleBuilder 类,
简化了这个过程,让您可以在几行代码中构建一个模块的“蓝图”。
例如,让我们以前面使用的示例(ConfigModule)为例,并将其转换为使用 ConfigurableModuleBuilder。
在开始之前,确保我们创建一个专门表示 ConfigModule 接受的选项的接口。
// interfaces/config-module-options.interface.ts
export interface ConfigModuleOptions {
folder: string;
}
有了这个接口,创建一个新的专用文件(与现有的 config.module.ts 文件并列),
并将其命名为 config.module-definition.ts。
在这个文件中,让我们利用 ConfigurableModuleBuilder 来构建 ConfigModule 的定义。
// config.module-definition.ts
import { ConfigurableModuleBuilder } from '@nestjs/common';
import { ConfigModuleOptions } from './interfaces/config-module-options.interface';
export const { ConfigurableModuleClass, MODULE_OPTIONS_TOKEN } =
new ConfigurableModuleBuilder<ConfigModuleOptions>().build();
现在,打开 config.module.ts 文件,并修改其实现以利用自动生成的 ConfigurableModuleClass。
// config.module.ts
import { Module } from '@nestjs/common';
import { ConfigService } from './config.service';
import { ConfigurableModuleClass } from './config.module-definition';
@Module({
providers: [ConfigService],
exports: [ConfigService],
})
export class ConfigModule extends ConfigurableModuleClass {}
扩展 ConfigurableModuleClass 意味着 ConfigModule 现在不仅提供了register方法
(与以前的自定义实现相同),还提供了 registerAsync 方法,允许使用者异步配置该模块,
例如,通过提供异步工厂:
// app.module.ts
@Module({
imports: [
ConfigModule.register({ folder: './config' }),
// 或者使用:
// ConfigModule.registerAsync({
// useFactory: () => {
// return {
// folder: './config',
// }
// },
// inject: [...任何额外的依赖项...]
// }),
],
})
export class AppModule {}
最后,让我们更新 ConfigService 类,以注入生成的模块选项的提供者,
而不是我们迄今为止使用的 'CONFIG_OPTIONS'。
// config.service.ts
import { Injectable, Inject } from '@nestjs/common';
import { ConfigModuleOptions } from './interfaces/config-module-options.interface';
import { MODULE_OPTIONS_TOKEN } from './config.module-definition';
@Injectable()
export class ConfigService {
constructor(@Inject(MODULE_OPTIONS_TOKEN) private options: ConfigModuleOptions) { ... }
}
自定义方法键
ConfigurableModuleClass 默认提供 register 及其对应的 registerAsync 方法。
要使用不同的方法名称,请使用 ConfigurableModuleBuilder#setClassMethodName 方法,
如下所示:
// config.module-definition.ts
export const { ConfigurableModuleClass, MODULE_OPTIONS_TOKEN } =
new ConfigurableModuleBuilder<ConfigModuleOptions>().setClassMethodName('forRoot').build();
这个构造将指示 ConfigurableModuleBuilder 生成一个公开 forRoot 和 forRootAsync 方法的类。
示例:
// app.module.ts
@Module({
imports: [
ConfigModule.forRoot({ folder: './config' }), // <-- 注意使用 "forRoot" 而不是 "register"
// 或者使用:
// ConfigModule.forRootAsync({
// useFactory: () => {
// return {
// folder: './config',
// }
// },
// inject: [...任何额外的依赖项...]
// }),
],
})
export class AppModule {}
自定义选项工厂类
由于 registerAsync 方法 (或 forRootAsync 或其他名称,取决于配置)
允许消费者传递一个解析为模块配置的提供程序定义,因此库的消费者可能会提供一个用于构造配置对象的类。
// app.module.ts
@Module({
imports: [
ConfigModule.registerAsync({
useClass: ConfigModuleOptionsFactory,
}),
],
})
export class AppModule {}
默认情况下,该类必须提供 create() 方法,该方法返回一个模块配置对象。
然而,如果您的库遵循不同的命名约定,您可以更改该行为并指示
ConfigurableModuleBuilder 期望一个不同的方法,
例如 createConfigOptions,使用 ConfigurableModuleBuilder#setFactoryMethodName 方法:
// config.module-definition.ts
export const { ConfigurableModuleClass, MODULE_OPTIONS_TOKEN } =
new ConfigurableModuleBuilder<ConfigModuleOptions>().setFactoryMethodName('createConfigOptions').build();
现在,ConfigModuleOptionsFactory 类必须公开 createConfigOptions 方法(而不是 create):
// config.module-options-factory.ts
export class ConfigModuleOptionsFactory {
createConfigOptions(): ConfigModuleOptions {
// 返回模块配置对象
return {
folder: './config',
};
}
}
然后在应用模块中使用:
// app.module.ts
@Module({
imports: [
ConfigModule.registerAsync({
useClass: ConfigModuleOptionsFactory, // <-- 此类必须提供 "createConfigOptions" 方法
}),
],
})
export class AppModule {}
额外选项
在某些情况下,您的模块可能需要获取额外的选项,以确定其应该如何行为(一个很好的例子是 isGlobal 标志 - 或者只是 global),
同时这些选项不应包含在 MODULE_OPTIONS_TOKEN 提供程序中(因为它们与在该模块中注册的服务/提供程序无关,
例如,ConfigService 不需要知道其宿主模块是否注册为全局模块)。
在这种情况下,可以使用 ConfigurableModuleBuilder#setExtras 方法。
请参阅以下示例:
export const { ConfigurableModuleClass, MODULE_OPTIONS_TOKEN } = new ConfigurableModuleBuilder<ConfigModuleOptions>()
.setExtras(
{
isGlobal: true,
},
(definition, extras) => ({
...definition,
global: extras.isGlobal,
}),
)
.build();
上面示例中,传递给 setExtras 方法的
第一个参数是一个包含“额外”属性默认值的对象。
第二个参数是一个函数,它接受一个自动生成的模块定义(具有提供程序、导出等)和一个额外的对象,
该对象表示额外属性(由消费者指定或默认值)。该函数的返回值是修改后的模块定义。
在此特定示例中,我们将 extras.isGlobal 属性取出并将其赋值给模块定义的 global 属性
(从而确定模块是否是全局模块,阅读更多信息请点击此处)。
现在,在使用该模块时,可以传入额外的 isGlobal 标志,如下所示:
@Module({
imports: [
ConfigModule.register({
isGlobal: true,
folder: './config',
}),
],
})
export class AppModule {}
但是,由于 isGlobal 被声明为“额外”属性,它不会出现在 MODULE_OPTIONS_TOKEN 提供程序中:
@Injectable()
export class ConfigService {
constructor(@Inject(MODULE_OPTIONS_TOKEN) private options: ConfigModuleOptions) {
// "options" 对象将不包含 "isGlobal" 属性
// ...
}
}
扩展自动生成的方法
如果需要,可以扩展自动生成的静态方法(例如,register、registerAsync 等),如下所示:
import { Module } from '@nestjs/common';
import { ConfigService } from './config.service';
import { ConfigurableModuleClass, ASYNC_OPTIONS_TYPE, OPTIONS_TYPE } from './config.module-definition';
@Module({
providers: [ConfigService],
exports: [ConfigService],
})
export class ConfigModule extends ConfigurableModuleClass {
static register(options: typeof OPTIONS_TYPE): DynamicModule {
return {
// 添加您的自定义逻辑
...super.register(options),
};
}
static registerAsync(options: typeof ASYNC_OPTIONS_TYPE): DynamicModule {
return {
// 添加您的自定义逻辑
...super.registerAsync(options),
};
}
}
请注意,OPTIONS_TYPE 和 ASYNC_OPTIONS_TYPE 类型的使用,这些类型必须从模块定义文件中导出:
export const { ConfigurableModuleClass, MODULE_OPTIONS_TOKEN, OPTIONS_TYPE, ASYNC_OPTIONS_TYPE } = new ConfigurableModuleBuilder<ConfigModuleOptions>().build();
注入作用域
对于来自不同编程语言背景的人来说,可能会感到意外的是,在Nest中,几乎所有内容都是跨传入请求共享的。 我们有一个与数据库的连接池、具有全局状态的单例服务等等。 请记住,Node.js不遵循请求/响应的多线程无状态模型,其中每个请求都由一个单独的线程处理。 因此,在我们的应用程序中使用单例实例是完全安全的。
然而,在一些特殊情况下,可能需要基于请求的生命 周期, 例如在GraphQL应用程序中进行每个请求的缓存、请求跟踪和多租户支持。 注入作用域提供了一种机制来获得所需的提供程序生命周期行为。
提供程序作用域
提供程序可以具有以下任何一种作用域:
-
DEFAULT: 提供程序的单个实例在整个应用程序中共享。实例的生命周期直接与应用程序生命周期相关联。 一旦应用程序启动,所有单例提供程序都已实例化。默认情况下使用单例作用域。 -
REQUEST: 为每个传入请求专门创建提供程序的新实例。在请求完成处理后,该实例将被垃圾回收。 -
TRANSIENT: 瞬时提供程序不在各个消费者之间共享。 每个注入瞬时提供程序的消费者都将获得一个新的、专用的实例。
对于大多数用例,建议使用单例作用域。 在消费者和请求之间共享提供程序意味着可以缓存实例,并且其初始化仅在应用程序启动期间发生一次。
使用方式
通过将scope属性传递给@Injectable()装饰器选项对象来指定注入作用域:
import { Injectable, Scope } from '@nestjs/common';
@Injectable({ scope: Scope.REQUEST })
export class CatsService {}
同样,对于自定义提供程序,可以在提供程序注册的长格式中设置scope属性:
{
provide: 'CACHE_MANAGER',
useClass: CacheManager,
scope: Scope.TRANSIENT,
}
从 @nestjs/common 中导入 Scope 枚举。
默认情况下使用单例作用域,并且无需声明。如果确实要将提供程序声明为单例作用域,
请对scope属性使用Scope.DEFAULT值。
WebSocket Gateways 不应使用请求作用域的提供程序,因为它们必须充当单例。 每个网关封装了一个真实的套接字,不能多次实例化。 此限制还适用于其他一些提供程序,例如 Passport 策略或 Cron 控制器。
控制器作用域
控制器也可以具有作用域,该作用域适用于在该控制器中声明的所有请求方法处理程序。 与提供程序作用域一样,控制器的作用域声明其生命周期。 对于请求作用域的控制器,将为每个传入请求创建一个新实例,并在请求处理完成后进行垃圾回收。
使用ControllerOptions对象的scope属性声明控制器作用域:
@Controller({
path: 'cats',
scope: Scope.REQUEST,
})
export class CatsController {}
作用域层次结构
REQUEST作用域在注入链上传播。依赖于请求作用域提供程序的控制器本身将成为请求作用域。
想象一下以下依赖图:CatsController <- CatsService <- CatsRepository。
如果CatsService是请求作用域(其他是默认的单例),
则CatsController将成为请求作用域,因为它依赖于注入的服务。
而不依赖的CatsRepository仍将保持单例作用域。
瞬时作用域的依赖关系不遵循该模式。
如果单例作用域的DogsService注入一个瞬时作用域的LoggerService提供程序,它将收到一个新实例。
然而,DogsService将保持单例作用域,因此在任何地方注入它都不会解析为DogsService的新实例。
如果希望达到这种行为,必须将DogsService明确标记为TRANSIENT。
请求提供程序
在基于 HTTP 服务器的应用程序中(例如使用 @nestjs/platform-express
或 @nestjs/platform-fastify),当使用请求作用域的提供程序时,
您可能希望访问对原始请求对象的引用。您可以通过注入REQUEST对象来实现这一点。
import { Injectable, Scope, Inject } from '@nestjs/common';
import { REQUEST } from '@nestjs/core';
import { Request } from 'express';
@Injectable({ scope: Scope.REQUEST })
export class CatsService {
constructor(@Inject(REQUEST) private request: Request) {}
}
由于底层平台/协议的差异,您在微服务或GraphQL应用程序中略有不同地访问传入请求。
在GraphQL应用程序中,您注入CONTEXT而不是REQUEST:
import { Injectable, Scope, Inject } from '@nestjs/common';
import { CONTEXT } from '@nestjs/graphql';
@Injectable({ scope: Scope.REQUEST })
export class CatsService {
constructor(@Inject(CONTEXT) private context) {}
}
然后,在GraphQLModule中配置上下文值,使其包含request作为其属性。