Nest.js 自定义提供者 Provider

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

DI 基础介绍

依赖注入是一种控制反转 (IoC) 技术,我们可以将依赖项的实例化委托给 IoC 容器(在我们的例子中是 NestJS 运行系统),而不是在自己的代码中强制执行。 让我们从 Providers 章节检查这个示例中发生了什么。

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

cats.service.ts

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

cats.controller.ts

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 令牌的依赖:
    constructor(private catsService: CatsService)
    
  3. 在 app.module.ts 中,我们将令牌 CatsService 与 cats.service.ts 文件中的 CatsService 类相关联。 我们将在下面看到这种关联(也称为注册)是如何发生的。

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

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


标准提供者 Provider

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

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

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

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

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


自定义 Provider

当我们的要求超出标准 provider 提供的要求时会发生什么? 这里有一些例子:

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

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

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

Value providers: useValue

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

import { CatsService } from './cats.service';

const mockCatsService = {
  /* 模拟实现
  ...
  */
};

@Module({
  imports: [CatsModule],
  providers: [
    {
      provide: CatsService,
      useValue: mockCatsService,
    },
  ],
})
export class AppModule {}

在此示例中,CatsService 令牌将解析为 mockCatsService 模拟对象。 useValue 需要一个值 - 在这种情况下,一个文字对象与它要替换的 CatsService 类具有相同的接口。 由于 TypeScript 的结构类型,我们可以使用任何具有兼容接口的对象,包括文字对象或用 new 实例化的类实例。

非基于类的提供者令牌

到目前为止,我们已经使用类名作为我们的提供者标记(providers 数组中列出的提供者中的 provide 属性的值)。 这与基于构造函数的注入使用的标准模式相匹配,其中 token 也是类名。 (如果此概念不完全清楚,请参阅 DI 基础知识来重新了解令牌)。 有时,我们可能希望灵活地使用字符串或符号作为 DI 令牌。 例如:

import { connection } from './connection';

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

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

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

@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 将以相同的顺序将注入列表中的实例作为参数传递给工厂函数。 下面的示例说明了这一点。
const connectionFactory = {
  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 resolves to `undefined`.
};

@Module({
  providers: [
    connectionFactory,
    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 {}

非基于服务的 provider

虽然 provider 经常提供服务,但他们并不局限于这种用途。 提供者可以提供任何东西。 例如,提供者可能会根据当前环境提供一组配置对象,如下所示:

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

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

导出自定义 provider

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

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

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

查看笔记

扫码一下
查看教程更方便