自定义提供者
在早期的章节中,我们涉及了**依赖注入(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 {}