服务提供者
Provider Service
字面直译是“服务提供者”,它是一个提供功能的类,为控制器服务(被调用)的都可以理解为是 Provider
。
用大白话来说 provider
是一个工具类,恰好被控制器调用且在 IoC
容器中注册的工具类。任何类都可以是 provider
,重点是你希望这个类交由 IoC
容器管理。
在之前我们学习了依赖注入 DI
和控制反转 IoC
概念,Nest
实现了 IoC
容器,会从入口模块开始扫描,分析 Module
之间的引用关系,对象之间的依赖关系,自动把 provider
注入到目标对象。
使用服务提供者
我们来创建一个简单的 Cat
模块,它的 CatService
负责提供数据存储和检索,并被 CatController
所调用。
使用 CLI
命令创建模块:
nest g res cats --no-spec
控制器调用依赖注入的类,使用背后的存储和检索方法:
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();
}
}
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;
}
}
export interface Cat {
name: string;
age: number;
breed: string;
}
先思考一下,为什么 CatService
不需要 new
实例,也能在控制器中使用呢?
这是因为它的实例是交由了 Nest
的 IoC
决定的,也就是在控制器实例化时,服务提供者就已经通过容器完成了依赖注入。
CatService
只需要在 Module
中完成注册,Nest
就会帮助它完成依赖注入。
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()
标记这个类为提供者。
// cats.service.ts
@Injectable()
export class CatsService {
// ...
}
接着提供者类要在 Module
中完成注册绑定
@Module({
controllers: [CatsController],
providers: [CatsService]
})
export class CatsModule {}
最后,这个注入的提供者就可以为控制器所使用,也就是通过构造函数注入到控制器中
@Controller("cats")
export class CatsController {
constructor(private catsService: CatsService) {}
// ...
}
简单解释下过程,当 Ioc
容器实例化 CatContrller
时,它会查找控制器的所有的依赖注入的服务。当找到 CatService
时,会把这个类当做一个 Token
去容器找找到与该令牌对应的类—— CatService
。容器还会根据服务的作用域 scope
进行下一步的判断,是否缓存当前服务。
标准的提供者——类提供者
@Module()
装饰器的 providers
元数据属性接受一个提供者数据。上面的例子我们用的是 providers:[CatService]
,实际上,该语法是下面的更完整语法的简写:
proviers: [
{
provide: CatService,
useClass: CatService
}
]
这样就能理解 会把这个类当做一个 Token
去容器找找到与该令牌对应的类—— CatService
这句话了。
值提供者——注入常量
你还可以使用 useValue
对其注入常量值,就像下面这样:
import { CatsService } from './cats.service';
const mockCatsService = {
/* mock implementation
...
*/
};
@Module({
imports: [CatsModule],
providers: [
{
provide: CatsService,
useValue: mockCatsService,
},
],
})
export class AppModule {}
除了字面量常量外,你还可以使用一些能得到常量值的方法,比如一个函数返回常量值:
import { CatsService } from "./cats.service";
const createMockService = () => {
return {
// ... mock service
};
};
@Module({
controllers: [CatsController],
providers: [
{
provide: CatsService,
useValue: createMockService()
}
]
})
export class CatsModule {}
非类提供者——使用非类作为令牌
依赖注入会根据注册时提供的 Token
返回对应的类或值,令牌可以是类,也可以是其他的字符串或符号:
import { connection } from './connection';
@Module({
providers: [
{
provide: 'CONNECTION',
useValue: connection,
},
],
})
export class AppModule {}
TIP
使用了非类作为提供者的令牌,在为控制器所使用时,要换一种方式获取注入的服务。(提取依赖会讲到)
工厂提供者——动态注册
工厂函数允许动态创建提供者,使用 useFactory
返回实际的提供者。这种语法提供了两个参数:
- 工厂函数可以接受参数。
inject
属性接受一个provider
数组,Nest
在实例化过程中解析这些提供者并将其作为参数传递给工厂函数的参数中。
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
:
@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
inject
的 provider
一般要完成注册,才能注入到工厂函数的参数中,否则依赖分析时会发生报错。
如果你把该配置设置为 optional: true
可选时,报错不会发生,此时工厂函数获取的值为 undefined
。
提取依赖
学习了如何注入依赖,接下来到如何提取依赖。
最常见的方式,通过构造函数提取注入的依赖(注入意味在 @Module()
的 provider
完成注册):
constructor(private catsService: CatsService) {}
当你使用非类作为令牌注入依赖时,构造函数提取的方式将会不可用,需要使用 @Inject(token)
才能进行正确的提取:
@Module({
controllers: [BirdController],
providers: [
BirdService,
// 注册使用非类的token注册的依赖服务
{
provide: "MOCK_TOKEN",
useValue: "mock token value"
}
]
})
export class BirdModule {}
@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
的依赖分析也能通过。
@Module({
controllers: [BirdController],
providers: [
BirdService,
// 未传递任何对象
// {
// provide: "MOCK_TOKEN",
// useValue: "mock token value"
// }
]
})
export class BirdModule {}
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;
}
}
别名提供者
别名提供者是一种语法,允许为现有的提供者创建别名,且别名后的提供者,和源提供者一起注入并不会报错。
@Injectable()
class LoggerService {
/* implementation details */
}
const loggerAliasProvider = {
provide: 'AliasedLoggerService',
useExisting: LoggerService,
};
@Module({
providers: [LoggerService, loggerAliasProvider],
})
export class AppModule {}
TIP
假设我们现在有两个不同的依赖,它们的作用域都指定为默认的单例模式,那么它们将会被解析为同一个实例。
导出自定义提供者
自定义提供者的作用域为当前模块,如果要使其对其他模块可用,必须要在 @Module
的 exports
元数据中导出。(下一章节会学习到模块)
以类作为令牌的,可以直接导出该类:
@Module({
controllers: [BirdController],
providers: [BirdService],
exports:[BirdService]
})
export class BirdModule {}
自定义令牌的提供者,需要导出它的令牌符号:
@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
, 并在其中定义一个私有变量,定义存取方法:
@Injectable()
export class AService {
private readonly list: Array<string> = [];
pushItem(item: string) {
this.list.push(item);
}
getList() {
return this.list;
}
}
定义两个控制器 B
和 C
,B
发送请求修改私有变量,C
紧接着获取这个变量,如果 C
获取的值,是 B
所修改的,说明单例模式是共享提供者的:
@Controller("b")
export class BController {
constructor(private readonly aService: AService) {}
@Get()
uodateList(@Query("value") value: string) {
// 对提供者进行存数据的操作
this.aService.pushItem(value);
return "success";
}
}
@Controller("c")
export class CController {
constructor(private readonly aService: AService) {}
@Get()
getList() {
// 对提供者取出数据
return this.aService.getList();
}
}
结果如下图,说明默认单例模式是共享模块服务的:
请求作用域——REQUEST
每次请求都会实例化提供者,并在请求结束后对这些实例进行垃圾回收。
还是刚才的例子,控制器们不做修改,只需要修改提供者 @Injectable()
的参数,修改为请求作用域:
@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
控制器获取的是默认值,也就是空数组:
瞬态作用域——TRANSIENT
每一个使用了该 provider
的控制器都会得到一个独立的提供者实例,服务隔离发生在模块层面。
在 B
控制器中新增修改私有变量方法,在 C
控制器中新增获取变量方法,也就说 B
和 C
除了请求路径不一样,剩余长的是一样的:
@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();
}
}
@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();
}
}
接下来的测试流程为:
- 请求
/b
路径,设置私有变量 - 分别请求
B
和C
中的获取变量的路由方法 - 保存结果,下一步请求
- 请求
/c
路径,设置私有变量 - 分别请求
B
和C
中的获取变量的路由方法 - 分析结果
对比两张结果,得出 B
控制器的设置私有变量操作,对 C
控制器的提供者无效。也就是提供者只共享同一个控制器,对于跨控制器,会创建不同的新提供者实例。
作用域冒泡
作用域冒泡是在注入时进行的。以下面的图作为例子
StorageService
分别被 AppModule
与 BookModule
导入使用,而 BookService
又被 AppModule
导入使用。
此时我们把 StorageService
的作用域设置为 REQUEST
,那么依赖于 ``StorageService 的
BookService和
AppService都会变成
REQUEST ,按照冒泡的逻辑来看,
AppController也会变成请求作用域,因为它依赖了
AppService` 。
它们的依赖关系会变为下图所示:
但如果把 BookService
设置为 REQUEST
,那么仅有 AppService
与 AppController
会变为 REQUEST
,因为 StorageService
不依赖于 BookService
。
这就是作用域冒泡,你必须非常谨慎地指定提供者的请求作用域。
除非你的提供者必须要求指定作用域,否则强烈建议使用默认的单例模式。
TIP
对于自定义提供者,如果想修改作用域,需要传递 scope
来指定作用域的范围。
{
provide: 'CACHE_MANAGER',
useClass: CacheManager,
scope: Scope.TRANSIENT,
}