Skip to content

服务提供者

Provider Service 字面直译是“服务提供者”,它是一个提供功能的类,为控制器服务(被调用)的都可以理解为是 Provider

用大白话来说 provider 是一个工具类,恰好被控制器调用且在 IoC 容器中注册的工具类。任何类都可以是 provider ,重点是你希望这个类交由 IoC 容器管理。

在之前我们学习了依赖注入 DI 和控制反转 IoC概念,Nest 实现了 IoC 容器,会从入口模块开始扫描,分析 Module 之间的引用关系,对象之间的依赖关系,自动把 provider 注入到目标对象。

使用服务提供者

我们来创建一个简单的 Cat 模块,它的 CatService 负责提供数据存储和检索,并被 CatController 所调用。

使用 CLI 命令创建模块:

shell
nest g res cats --no-spec

控制器调用依赖注入的类,使用背后的存储和检索方法:

ts
import { Body, Controller, Get, Post } from "@nestjs/common";
import { CatsService } from "./cats.service";
import { Cat } from "./interfaces/cat.interface";
import { CreateCatDto } from "./dto/create-cat.dto";

@Controller("cats")
export class CatsController {
  constructor(private catsService: CatsService) {}

  @Post()
  async create(@Body() createCatDto: CreateCatDto) {
    this.catsService.create(createCatDto);
  }

  @Get()
  async findAll(): Promise<Cat[]> {
    return this.catsService.findAll();
  }
}
ts
import { Injectable } from "@nestjs/common";
import { Cat } from "./interfaces/cat.interface";

@Injectable()
export class CatsService {
  private readonly cats: Cat[] = [];

  create(cat: Cat) {
    this.cats.push(cat);
  }

  findAll(): Cat[] {
    return this.cats;
  }
}
ts
export interface Cat {
  name: string;
  age: number;
  breed: string;
}

先思考一下,为什么 CatService 不需要 new 实例,也能在控制器中使用呢?

这是因为它的实例是交由了 NestIoC 决定的,也就是在控制器实例化时,服务提供者就已经通过容器完成了依赖注入。

CatService 只需要在 Module 中完成注册,Nest 就会帮助它完成依赖注入。

ts
import { Module } from "@nestjs/common";
import { CatsService } from "./cats.service";
import { CatsController } from "./cats.controller";

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

依赖注入

依赖注入是一种控制反转的技术,我们将依赖的实例化交给 IoC 容器,而不是在开发人员 new 实例。接下来我们分析一下从使用提供者到依赖注入的这个过程:

首先,我们定义一个提供者,使用 @Injectable() 标记这个类为提供者。

ts
// cats.service.ts
@Injectable()
export class CatsService {
 // ...
}

接着提供者类要在 Module 中完成注册绑定

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

最后,这个注入的提供者就可以为控制器所使用,也就是通过构造函数注入到控制器中

ts
@Controller("cats")
export class CatsController {
  constructor(private catsService: CatsService) {}
  // ...
}

简单解释下过程,当 Ioc 容器实例化 CatContrller 时,它会查找控制器的所有的依赖注入的服务。当找到 CatService时,会把这个类当做一个 Token 去容器找找到与该令牌对应的类—— CatService 。容器还会根据服务的作用域 scope 进行下一步的判断,是否缓存当前服务。

标准的提供者——类提供者

@Module() 装饰器的 providers 元数据属性接受一个提供者数据。上面的例子我们用的是 providers:[CatService] ,实际上,该语法是下面的更完整语法的简写:

ts
proviers: [
  {
    provide: CatService,
    useClass: CatService
  }
]

这样就能理解 会把这个类当做一个 Token 去容器找找到与该令牌对应的类—— CatService这句话了。

值提供者——注入常量

你还可以使用 useValue 对其注入常量值,就像下面这样:

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

const mockCatsService = {
  /* mock implementation
  ...
  */
};

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

除了字面量常量外,你还可以使用一些能得到常量值的方法,比如一个函数返回常量值:

ts
import { CatsService } from "./cats.service";

const createMockService = () => {
  return {
    // ... mock service
  };
};

@Module({
  controllers: [CatsController],
  providers: [
    {
      provide: CatsService,
      useValue: createMockService()
    }
  ]
})
export class CatsModule {}

非类提供者——使用非类作为令牌

依赖注入会根据注册时提供的 Token 返回对应的类或值,令牌可以是类,也可以是其他的字符串或符号:

ts
import { connection } from './connection';

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

TIP

使用了非类作为提供者的令牌,在为控制器所使用时,要换一种方式获取注入的服务。(提取依赖会讲到)

工厂提供者——动态注册

工厂函数允许动态创建提供者,使用 useFactory 返回实际的提供者。这种语法提供了两个参数:

  1. 工厂函数可以接受参数。
  2. inject 属性接受一个 provider 数组,Nest 在实例化过程中解析这些提供者并将其作为参数传递给工厂函数的参数中。
ts
import { Injectable, Module } from "@nestjs/common";
import { DogsService } from "./dogs.service";
import { DogsController } from "./dogs.controller";

@Injectable()
class ConfigOptionProvider {
  private readonly config: Record<string, any> = {
    key1: "value1",
    key2: "value2"
  };

  get(key: string) {
    return this.config[key];
  }
}

@Module({
  controllers: [DogsController],
  providers: [
    DogsService,
    ConfigOptionProvider, // 要想在inject数组中传递,必须要完成注册
    {
      provide: "FACTORY_PROVIDER_TOKEN",
      useFactory: (config: ConfigOptionProvider) => {
        const options = config.get("key1");
      },
      inject: [ConfigOptionProvider]
    }
  ]
})
export class DogsModule {}

inject 数组还接受一些可选的配置,当该依赖没有传递时,获取的值是 undefined

ts
@Module({
  controllers: [DogsController],
  providers: [
    DogsService,
    ConfigOptionProvider,
    // {provide:"doSomeThing",useValue:"doSomeThing"}, // 即使依赖没有完成注册也不会报错,因为该提供者是可选的
    {
      provide: "FACTORY_PROVIDER_TOKEN",
      useFactory: (config: ConfigOptionProvider, optionalProvider?: string) => {
        const options = config.get("key1");
        
        console.log(optionalProvider); // undefined
      },
      inject: [ConfigOptionProvider, { token: "doSomeThing", optional: true }]
    }
  ]
})
export class DogsModule {}

TIP

injectprovider 一般要完成注册,才能注入到工厂函数的参数中,否则依赖分析时会发生报错。

如果你把该配置设置为 optional: true 可选时,报错不会发生,此时工厂函数获取的值为 undefined

提取依赖

学习了如何注入依赖,接下来到如何提取依赖。

最常见的方式,通过构造函数提取注入的依赖(注入意味在 @Module()provider 完成注册):

ts
constructor(private catsService: CatsService) {}

当你使用非类作为令牌注入依赖时,构造函数提取的方式将会不可用,需要使用 @Inject(token) 才能进行正确的提取:

ts
@Module({
  controllers: [BirdController],
  providers: [
    BirdService,
    // 注册使用非类的token注册的依赖服务
    {
      provide: "MOCK_TOKEN",
      useValue: "mock token value"
    }
  ]
})
export class BirdModule {}
ts
@Controller("borid")
export class BirdController {
  constructor(
    private readonly boridService: BirdService,
    // 使用 @Inject 提取依赖
    @Inject("MOCK_TOKEN") private readonly mockTokenService: string
  ) {}

  @Get()
  find() {
    return this.mockTokenService;
  }
}

可选提供者

我们在 useFactory 例子中,inject 数组中使用了可选的提供者配置。不止在 inject 能使用可选配置,在构造提取依赖时,也可以使用 @Optional() 指定可选提供者。

比如你的提供者可能依赖于配置对象,但如果未传递任何对象,则应该使用默认值,Nest 的依赖分析也能通过。

ts
@Module({
  controllers: [BirdController],
  providers: [
    BirdService,
    // 未传递任何对象
    // {
    //   provide: "MOCK_TOKEN",
    //   useValue: "mock token value"
    // }
  ]
})
export class BirdModule {}
ts
interface AppConfig {
  apiKey: string;
  apiUrl: string;
}

@Controller("bird")
export class BirdController {
  constructor(
    private readonly boridService: BirdService,
     // 指定当前依赖为可选,当未传递时,使用参数默认值,参数没有默认值时,值为undefined
    @Optional()
    @Inject("MOCK_TOKEN")
    private readonly mockTokenService: AppConfig = { apiKey: "apiKey", apiUrl: "apiUrl" }
  ) {}

  @Get()
  find() {
    console.log(this.mockTokenService);
    return this.mockTokenService;
  }
}

别名提供者

别名提供者是一种语法,允许为现有的提供者创建别名,且别名后的提供者,和源提供者一起注入并不会报错。

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

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

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

TIP

假设我们现在有两个不同的依赖,它们的作用域都指定为默认的单例模式,那么它们将会被解析为同一个实例。

导出自定义提供者

自定义提供者的作用域为当前模块,如果要使其对其他模块可用,必须要在 @Moduleexports 元数据中导出。(下一章节会学习到模块

以类作为令牌的,可以直接导出该类:

ts
@Module({
  controllers: [BirdController],
  providers: [BirdService],
  exports:[BirdService]
})
export class BirdModule {}

自定义令牌的提供者,需要导出它的令牌符号:

ts
@Module({
  controllers: [BirdController],
  providers: [
    BirdService,
    {
      provide: "MOCK_TOKEN",
      useValue: "mock token value"
    }
  ],
  // 导出令牌符号
  exports: ["MOCK_TOKEN"]
})
export class BirdModule {}

作用域scope

Nest 应用,提供者们默认的作用域是单例模式,这意味着在整个 Nest 应用周期内,提供者只会被实例化一次,所有的请求共享相同的提供者。

这种设计减少了 provider 的实例化次数,可以更好地提高程序性能。但是这种设计无法满足一些特定的需求,比如每一次请求,都需要知道用户请求的时间、IP地址、比如服务器需要与第三方API进行交互。

单例作用域——DEFAULT

整个 Nest 应用周期只会实例化一次,所有模块会共享服务。

例如我们存在一个 AService, 并在其中定义一个私有变量,定义存取方法:

ts
@Injectable()
export class AService {
  private readonly list: Array<string> = [];

  pushItem(item: string) {
    this.list.push(item);
  }

  getList() {
    return this.list;
  }
}

定义两个控制器 BCB 发送请求修改私有变量,C 紧接着获取这个变量,如果 C 获取的值,是 B 所修改的,说明单例模式是共享提供者的:

ts
@Controller("b")
export class BController {
  constructor(private readonly aService: AService) {}

  @Get()
  uodateList(@Query("value") value: string) {
    // 对提供者进行存数据的操作
    this.aService.pushItem(value);

    return "success";
  }
}
ts
@Controller("c")
export class CController {
  constructor(private readonly aService: AService) {}

  @Get()
  getList() {
    // 对提供者取出数据
    return this.aService.getList();
  }
}

结果如下图,说明默认单例模式是共享模块服务的:

image-20240130001547269

请求作用域——REQUEST

每次请求都会实例化提供者,并在请求结束后对这些实例进行垃圾回收。

还是刚才的例子,控制器们不做修改,只需要修改提供者 @Injectable() 的参数,修改为请求作用域:

ts
@Injectable({ scope: Scope.REQUEST })
export class AService {
  private readonly list: Array<string> = [];

  pushItem(item: string) {
    this.list.push(item);
  }

  getList() {
    return this.list;
  }
}

TIP

Scope 枚举由 @nestjs/common 包中导出。

我们按照刚才的流程,重新请求一遍,验证在修改私有变量后,获取的是否为最新的值。

结果如下图,正如我们期望的一样,C 控制器获取的是默认值,也就是空数组:

image-20240130002514868

瞬态作用域——TRANSIENT

每一个使用了该 provider 的控制器都会得到一个独立的提供者实例,服务隔离发生在模块层面。

B 控制器中新增修改私有变量方法,在 C 控制器中新增获取变量方法,也就说 BC 除了请求路径不一样,剩余长的是一样的:

ts
@Controller("b")
export class DController {
  constructor(private readonly aService: AService) {}

  @Get()
  uodateList(@Query("value") value: string) {
    console.log(value);
    this.aService.pushItem(value);

    return "success";
  }

  @Get("list")
  getList() {
    return this.aService.getList();
  }
}
ts
@Controller("c")
export class CController {
  constructor(private readonly aService: AService) {}

  @Get()
  uodateList(@Query("value") value: string) {
    console.log(value);
    this.aService.pushItem(value);

    return "success";
  }

  @Get("list")
  getList() {
    return this.aService.getList();
  }
}

接下来的测试流程为:

  1. 请求 /b 路径,设置私有变量
  2. 分别请求 BC 中的获取变量的路由方法
  3. 保存结果,下一步请求

image-20240130005338202

  1. 请求 /c 路径,设置私有变量
  2. 分别请求 BC 中的获取变量的路由方法
  3. 分析结果

image-20240130005428940

对比两张结果,得出 B 控制器的设置私有变量操作,对 C 控制器的提供者无效。也就是提供者只共享同一个控制器,对于跨控制器,会创建不同的新提供者实例。

作用域冒泡

作用域冒泡是在注入时进行的。以下面的图作为例子

201193381IQZTZrN60

StorageService 分别被 AppModuleBookModule 导入使用,而 BookService 又被 AppModule 导入使用。

此时我们把 StorageService 的作用域设置为 REQUEST ,那么依赖于 ``StorageServiceBookServiceAppService都会变成REQUEST ,按照冒泡的逻辑来看,AppController也会变成请求作用域,因为它依赖了AppService` 。

它们的依赖关系会变为下图所示:

201193380Ba7pCPiPN

但如果把 BookService 设置为 REQUEST ,那么仅有 AppServiceAppController 会变为 REQUEST ,因为 StorageService 不依赖于 BookService

201193387hraBJGFfQ

这就是作用域冒泡,你必须非常谨慎地指定提供者的请求作用域。

除非你的提供者必须要求指定作用域,否则强烈建议使用默认的单例模式。


TIP

对于自定义提供者,如果想修改作用域,需要传递 scope 来指定作用域的范围。

ts
{
  provide: 'CACHE_MANAGER',
  useClass: CacheManager,
  scope: Scope.TRANSIENT,
}

如有转载或 CV 的请标注本站原文地址