自定义提供程序 Custom providers

导读

在前面的章节中,我们谈到了依赖注入 (DI) 的各个方面以及它在 Nest 中的使用方式。其中一个例子是 基于构造函数的 依赖注入,用于将实例(通常是服务提供者)注入类。您不会惊讶地发现依赖注入以基本方式内置于 Nest 核心中。到目前为止,我们只探索了一种主要模式。随着应用程序变得越来越复杂,您可能需要利用 DI 系统的全部功能,因此让我们更详细地探索它们。

DI 基础知识

依赖注入是一种 控制反转 (IoC) 技术,其中您将依赖项的实例化委托给 IoC 容器(在我们的例子中是 NestJS 运行时系统),而不是在您自己的代码中命令式地执行。让我们从 Providers 章节 中查看此示例中发生的情况。

首先,我们定义一个提供程序。@Injectable() 装饰器将 CatsService 类标记为提供程序。

ts
TS
ts
// cats.service
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 将提供程序注入到我们的控制器类中:

ts
TS
ts
// cats.controller
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 容器注册提供程序:

app.module
ts
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 {}

幕后究竟发生了什么事情才能实现这一点?这个过程有三个关键步骤:

  1. cats.service.ts 中,@Injectable() 装饰器将 CatsService 类声明为可由 Nest IoC 容器管理的类。
  2. cats.controller.ts 中,CatsController 使用构造函数注入声明对 CatsService 令牌的依赖:
ts
constructor(private catsServiceCatsService)
  1. app.module.ts 中,我们将令牌 CatsServicecats.service.ts 文件中的类 CatsService 关联。我们将在下面看到这种关联(也称为_注册_)是如何发生的。

当 Nest IoC 容器实例化 CatsController 时,它首先查找任何依赖项*。当它找到 CatsService 依赖项时,它会根据注册步骤(上面的 #3)对 CatsService 令牌执行查找,该令牌返回 CatsService 类。假设 SINGLETON 范围(默认行为),Nest 将创建 CatsService 的实例,缓存它并返回它,或者如果已经缓存了一个实例,则返回现有实例。

*这个解释有点简化以说明这一点。我们忽略的一个重要领域是分析代码的依赖关系的过程非常复杂,并且发生在应用程序引导期间。一个关键特性是依赖关系分析(或创建依赖关系图)是传递的。在上面的例子中,如果 CatsService 本身有依赖关系,那么这些依赖关系也会被解析。依赖关系图确保以正确的顺序解析依赖关系 - 本质上是自下而上。这种机制使开发人员免于管理这种复杂的依赖图。

Standard providers

让我们仔细看看 @Module() 装饰器。在 app.module 中,我们声明:

ts
@Module({
  controllers: [CatsController],
  providers: [CatsService],
})

providers 属性采用 providers 数组。到目前为止,我们通过类名列表提供了这些提供程序。事实上,语法 providers: [CatsService] 是更完整语法的简写:

ts
providers: [
  {
    provide: CatsService,
    useClass: CatsService,
  },
]

现在我们看到了这个显式构造,我们可以理解注册过程。在这里,我们清楚地将令牌CatsService与类CatsService关联起来。简写符号只是为了简化最常见的用例,其中令牌用于请求同名类的实例。

自定义提供程序

当您的要求超出_标准提供程序_提供的要求时会发生什么?以下是几个示例:

  • 您想创建一个自定义实例,而不是让 Nest 实例化(或返回缓存的实例)类
  • 您想在第二个依赖项中重用现有类
  • 您想用模拟版本覆盖类以进行测试

Nest 允许您定义自定义提供程序来处理这些情况。它提供了几种定义自定义提供程序的方法。让我们来看看。

提示

如果您在依赖项解析方面遇到问题,您可以设置NEST_DEBUG环境变量并在启动期间获取额外的依赖项解析日志。

值提供程序:useValue

useValue 语法对于注入常量值、将外部库放入 Nest 容器或用模拟对象替换实际实现非常有用。假设您想强制 Nest 使用模拟 CatsService 进行测试。

ts
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 基础知识以重新了解 token)。有时,我们可能希望灵活地使用字符串或符号作为 DI token。例如:

ts
import { connection } from './connection'

@Module({
  providers: [
    {
      provide: 'CONNECTION',
      useValue: connection,
    },
  ],
})
export class AppModule {}

在此示例中,我们将字符串值标记(CONNECTION)与从外部文件导入的预先存在的connection对象关联起来。

通知

除了使用字符串作为标记值之外,您还可以使用 JavaScript 符号 或 TypeScript 枚举

我们之前已经了解了如何使用标准基于构造函数的注入 模式注入提供程序。此模式要求使用类名声明依赖项。CONNECTION自定义提供程序使用字符串值标记。让我们看看如何注入这样的提供程序。为此,我们使用 @Inject() 装饰器。此装饰器接受一个参数 - 令牌。

ts
TS
ts
@Injectable()
export class CatsRepository {
  constructor(@Inject('CONNECTION') connection: Connection) {}
}
提示

@Inject() 装饰器从 @nestjs/common 包导入。

虽然我们在上面的示例中直接使用字符串 'CONNECTION' 来进行说明,但为了清晰地组织代码,最好在单独的文件中定义标记,例如 constants.ts。将它们视为在自己的文件中定义并在需要时导入的符号或枚举。

类提供程序:useClass

useClass 语法允许您动态确定标记应解析为的类。例如,假设我们有一个抽象(或默认)ConfigService 类。根据当前环境,我们希望 Nest 提供配置服务的不同实现。以下代码实现了这种策略。

ts
const configServiceProvider = {
  provide: ConfigService,
  useClass:
    process.env.NODE_ENV === 'development'
      ? DevelopmentConfigService
      : ProductionConfigService,
}

@Module({
  providers: [configServiceProvider],
})
export class AppModule {}

让我们看一下此代码示例中的几个细节。您会注意到,我们首先使用文字对象定义 configServiceProvider,然后将其传递给模块装饰器的 providers 属性。这只是代码组织的一小部分,但在功能上等同于我们在本章中迄今为止使用的示例。

此外,我们使用 ConfigService 类名作为我们的令牌。对于任何依赖于 ConfigService 的类,Nest 将注入所提供类的实例(DevelopmentConfigServiceProductionConfigService),覆盖可能已在其他地方声明的任何默认实现(例如,使用 @Injectable() 装饰器声明的 ConfigService)。

工厂提供程序:useFactory

useFactory 语法允许动态创建提供程序。实际提供程序将由工厂函数返回的值提供。工厂函数可以根据需要简单或复杂。简单工厂可能不依赖任何其他提供程序。更复杂的工厂本身可以注入计算结果所需的其他提供程序。对于后一种情况,工厂提供程序语法有一对相关机制:

  1. 工厂函数可以接受(可选)参数。
  2. (可选)inject 属性接受提供程序数组,Nest 将在实例化过程中解析这些提供程序并将其作为参数传递给工厂函数。此外,这些提供程序可以标记为可选。这两个列表应该相互关联:Nest 将以相同的顺序将 inject 列表中的实例作为参数传递给工厂函数。下面的示例演示了这一点。
ts
const connectionProvider = {
  provide: 'CONNECTION',
  useFactory: (optionsProvider: OptionsProvider, optionalProvider?: string) => {
    const options = optionsProvider.get()
    return new DatabaseConnection(options)
  },
  inject: [OptionsProvider, { token: 'SomeOptionalProvider', optional: true }],
  //       \_____________/            \__________________/
//            此提供程序                 具有此提供程序token
//            是强制性的。               可以解析为`undefined`。
}

@Module({
  providers: [
    connectionProvider,
    OptionsProvider,
    // { provide: 'SomeOptionalProvider', useValue: 'anything' },
  ],
})
export class AppModule {}
js
const connectionProvider = {
  provide: 'CONNECTION',
  useFactory: (optionsProvider, optionalProvider) => {
    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范围指定,则它们都将解析为同一个实例。

ts
@Injectable()
class LoggerService {
  /* implementation details */
}

const loggerAliasProvider = {
  provide: 'AliasedLoggerService',
  useExisting: LoggerService,
}

@Module({
  providers: [LoggerService, loggerAliasProvider],
})
export class AppModule {}

非基于服务的提供商

虽然提供商通常提供服务,但它们的用途并不局限于此。提供商可以提供任何值。例如,提供商可以根据当前环境提供一组配置对象,如下所示:

ts
const configFactory = {
  provide: 'CONFIG',
  useFactory: () => {
    return process.env.NODE_ENV === 'development' ? devConfig : prodConfig
  },
}

@Module({
  providers: [configFactory],
})
export class AppModule {}

导出自定义提供程序

与任何提供程序一样,自定义提供程序的作用域仅限于其声明模块。要使其对其他模块可见,必须将其导出。要导出自定义提供程序,我们可以使用它的令牌或完整的提供程序对象。

以下示例显示了使用令牌进行导出:

ts
TS
ts
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 {}

或者,使用完整的提供程序对象导出:

ts
TS
ts
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 {}