【NestJS】Custom providers

2,912 阅读8分钟

正文 Custom providers

Custom providers 自定义供应商

在前面的章节,我们提到了依赖注入(Dependency Injection)的诸多方面以及如何在Nest中使用它。其中一个例子就是基于构造函数的依赖注入用来将实例(通常是服务供应商)注入类中。当你了解到依赖注入是以一个基本的方式构于Nest核心时,你不会感到惊讶。目前为止,我们只探索了一个主要的部分。当你的应用越来越复杂,你可能需要利用依赖注入系统的全部特性,所以让我们更详细地探讨它。



DI fundamentals 依赖注入基本原理

依赖注入是一种反向控制技术,体现在你委托依赖项的实例化工作给一个IoC(inversion of control 反向控制)容器(在我们的例子中为NestJS运行系统)而不是通过自己的代码中去实现。让我们测试一下在这样的案例中发生了什么。
首先我们定义一个供应商, @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 readonly 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 readonly catsService: CatsService)
  1. app.module.ts文件中,我们将标识 CatsServicecats.service.ts文件中的类 CatsService 联系起来。下面我们将看到这种关联(也叫注册)是如何发生的。

当Nest的IoC容器实例化 CatsController 时,它首先会寻找是否有任何依赖项[*]。当它找到了 CatsService 依赖,就会按照上面注册的步骤#3对返回 CatsService 类的标识 CatsService 进行查找。假设单例作用域(默认行为),Nest将创建一个 CatsService 的实例,缓存并返回它,或者已经有缓存时直接返回存在的实例。

* 为了说明这一点,这个解释有一些简化。我们忽略的一个重要的方面是分析依赖项代码的过程是非常复杂的,并且这发生在应用启动的时候。一个关键的特性是依赖项分析(或者说是创建依赖图表)是可传递的。在上面的例子中,如果 CatsService 它本身含有依赖项,那么这些依赖项也会被解析。依赖图表确保了依赖项按照正确地顺序被解析——自下而上。这个技巧可以使开发者不用管理如此复杂的依赖图表。



Standard providers 标准供应商

让我们近距离看看 @Module() 装饰器。在app.module中我们声明:

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

providers属性的值为一个供应商的数组。目前我们通过类名的列表提供那些供应商。实际上,语法providers: [CatsService]是对更加完整语法的一个简写:

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

现在看这个详细的结构,我们可以理解注册的过程。这里清楚地看见,我们将标识 CatsService 和类 CatsService 关联起来。这个简写仅仅是为了当标识需要一个同名的实例,来简化最普遍的用例。



Custom providers 自定义供应商

当你的需求超过了那些标准供应商所提供的时候发生了什么?这有一些例子:

  • 你想创建一个自定义的实例而不是让Nest实例化(或者返回一个缓存的实例)。
  • 你想要在第二个依赖中重用一个现有的类。
  • 你想要重载一个类用于测试的模拟版本。

Nest允许你定义自定义供应商来处理这些情况。它提供了几种方式来定义自定义供应商。



Value providers: 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 具有相同的接口。由于TS的结构型态,你可以使用接口兼容的任何对象,包括字面量对象或者使用new实例化的类的实例。



Non-class-based provider tokens 不基于类的供应商的标识

目前我们使用的都是类名作为供应商的标识(在providers数组中的供应商列表中的provide属性的值),这是与基于构造函数注入使用的标准模式相匹配的,标识也是一个类名。有时我们可能想灵活地使用字符或者symbols作为依赖注入地标识。例如:

import { connection } from './connection'

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

在这个例子中,我们将一个值为字符的标识('CONNECTION')和一个从外部引入的connection对象关联起来。

NOTICE
除了使用字符作为标识的值以外,你也可以使用js的symbols。


我们之前看到了如何使用标准的基于构造函数注入模式的方式注入一个供应商。这个模式需要用类名声明的依赖。 CONNECTION这个 自定义供应商使用了值为字符的标识。让我们看看如何注入这样一个供应商。我们使用 @Inject() 装饰器来做这件事,装饰器接受单个参数——标识。

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

Hints
import { Inject } from '@nestjs/common'


虽然上面的例子中我们为了说明直接使用字符`'CONNECTION'`,但是为了清晰的代码组织,最好在分开的文件中定义标识,例如`constants.ts`。把他们看作是symbols或者枚举类型来处理,定义在各自的文件中,在需要的时候引入。



Class providers: 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 )



Factory providers: useFactory 工厂供应商

useFactory语法允许动态地创建供应商。实际的供应商将由一个工厂函数返回的值来提供。工厂函数可以根据需要简单或者复杂。一个简单的工厂可能不需要依赖任何其他的供应商。一个更加复杂的工厂可以自己注入其他的供应商来计算结果。后者,工厂供应商的语法有一对相关的机制。

  1. 工厂函数可以接受参数
  2. inject这个可选属性接受一个供应商的数组,Nest将解析并且在实例化过程中作为参数传入工厂函数。这两个列表应当是相关的:Nest将以相同的顺序将实例作为参数从inject列表传入到工厂函数中。
const connectionFactory = {
  provide: 'CONNECTION',
  useFactory: (optionsProvider: OptionsProvider) => {
    const options = optionsProvider.get();
    return new DatabaseConnection(options);
  },
  inject: [OptionsProvider],
}

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



Alias providers: useExisting 别名供应商

useExisting语法允许你为现有的供应商创建别名。这将创建两种方式来访问同一个供应商。在下面的例子中,字符标识 AliasedLoggerService 是类标识 LoggerService 的别名。假设我们有两个不同的依赖,一个使用 AliasedLoggerService ,另一个使用 LoggerService 。如果两个依赖项都为单例作用域,它们将解析为同一个实例。

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

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

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



Non-service based providers 非服务的供应商

虽然供应商通常提供服务,但他们并不限于这种用法。供应商可以提供任何值。例如,一个供应商可能提供一个基于当前环境的配置对象的数组

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

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



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