最近在写前面两篇关于依赖注入的文章时,我总是在想用一句怎么的话来简单而朴素的描述依赖注入的概念,让从来没接触过的朋友能比较形象的去理解。想来想去,觉得可以站在依赖注入容器的角度说:
你负责告诉我你需要什么(依赖),我负责给你送来什么(注入)
建议多读几遍上面这句话,再回头去阅读前面两篇文章,我觉得你会有更多的收获。
其实在前两篇文章中,关于NestJS依赖注入功能相关的内容已经介绍的差不多了,如果你掌握了的话,已可以顺利的用于实际的开发工作。今天想给大家介绍的是一些关于依赖注入的零碎遗留内容,在日常开发中也会遇到,但不是那么高频。主要有以下几点:
- 异步资源提供者
- 循环依赖问题与解决方式
- 注入范围
异步资源提供者
顾名思义,其实就是在资源创建的时候,存在异步的环节。比如在创建资源的时候,需要先访问一个后端API来获取一些配置信息,然后根据这些配置信息再做进一步的资源创建。这里的后端API访问就是一个异步的动作,这会导致整个资源创建流程也是异步的了。
在NestJS中,大多数的资源提供者都是只支持同步,比如ValueProvider和ClassProvider,能支持异步的只有FactoryProvider。用法其实挺简单的:
代码语言:javascript复制{
provide: ProductService,
useFactory: async () => {
// 调用远程接口获取信息
const configInfo = await getProductServiceConfig()
// 根据远程返回的数据作进一步实例化
if (configInfo.category) {
return new ProductService(configInfo.category)
} else {
return new ProductService()
}
}
}
如上所示,直接将原先useFactory指定的工厂函数声明成async方式的函数,就可以支持异步的创建流程了。
循环依赖问题与解决方式
所谓的循环依赖,就是指两个类之间存在互相依赖的情况,例如:资源A依赖资源B,资源B也需要依赖A,这种情况下,无论是在创建A还是创建B的时候,其实彼此都还不存在,也就是互相找不到对方来满足依赖,这就会发生错误。
在模块之间或提供者之间的嵌套都可能会出现循环依赖关系。通常情况下,我们在设计的时候应该尽量避免循环依赖,但是总有避免不了的情况,在NestJS中提供了一种称为前向引用(forward referencing)的技术来解析循环依赖项。
例如下面示例代码:
代码语言:javascript复制@Injectable()
export class CategoryService {
constructor(
@Inject(forwardRef(() => ProductService))
private readonly productService: ProductService,
) {}
}
代码语言:javascript复制@Injectable()
export class ProductService {
constructor(
@Inject(forwardRef(() => CategoryService))
private readonly categoryService: CategoryService,
) {}
}
以上的2个类之间有互相依赖关系,各自需要注入对方。如果未使用代码中NestJS框架提供的forwardRef()工具函数,就会报错提示找不到依赖的资源;而使用后,容器可以正确处理互相使用forwardRef()函数标记过的类。
该工具函数也可作用于2个模块之间,解决模块间的循环依赖:
代码语言:javascript复制@Module({
imports: [forwardRef(() => CategoryModule)],
})
export class ProductModule {}
代码语言:javascript复制@Module({
imports: [forwardRef(() => ProductModule)],
})
export class CategoryModule {}
除了使用上面提到的 forwardRef() 工具函数,NestJS还另外提供了一种可行的方式来解决循环依赖,那就是模块引用(Module Reference)。模块引用解决问题的思路是:不通过容器的自动依赖注入,而由我们自己来控制。
通过在类中注入框架提供的ModuleRef,并在模块初始化的生命周期函数中进行手动查找所需要的资源实例,就能避免自动注入时的尴尬问题:
代码语言:javascript复制import { Injectable, OnModuleInit } from '@nestjs/common';
import { ProductService } from './product.service';
import { ModuleRef } from '@nestjs/core';
@Injectable()
export class CategoryService implements OnModuleInit {
private productService: ProductService;
// 注入框架提供的ModuleRef实例
constructor(private readonly moduleRef: ModuleRef) { }
onModuleInit() {
//使用 moduleRef 从当前模块中查询 ProductService 资源实例
this.productService = this.moduleRef.get(ProductService);
}
}
注入范围
默认情况下,NestJS容器中创建的资源对象都是单例的。受益于Node.js的单进程模型,单例模式在NestJS下的使用是非常安全的,不像其他多线程语言对单例的访问操作会存在线程安全问题。因此,在绝大多数情况下,我们的NestJS程序在资源创建这块,都推荐使用默认的单例方式。
这种方式,其实也代表了资源的生存范围(Scope)。比如单例的话,是在应用启动后就被初始化,一直到应用关闭。
既然有单例方式,那肯定还有其他方式的存在。NestJS提供了3种范围:
- 单例(SINGLETON)- 应用一启动就被实例化,只有一个对象实例,在整个应用程序范围内被共享
- 请求(REQUEST)- 针对于每个请求生成一个实例,请求处理结束后销毁
- 零时(TRANSIENT)- 为每个资源消费者生成一个专用实例
我们可以在类的@Injectable装饰器中指定范围:
代码语言:javascript复制import { Injectable, Scope } from '@nestjs/common';
@Injectable({ scope: Scope.REQUEST })
export class MyService {}
也可以在定义资源提供者的地方指定范围:
代码语言:javascript复制{
provide: 'MY_MANAGER',
useClass: MyManager,
scope: Scope.TRANSIENT,
}
另外,资源依赖路径上的范围会有层级关系,是一个从底至上的冒泡关系,比如下面这样一个A依赖B,B依赖C的关系中:
代码语言:javascript复制AService <- BService <- CService
如果我们指定BService的范围为REQUEST,那么上层的AService也会变成REQUEST的,而下层的CService则仍保持默认的SINGLETON。
如果没有特别的原因,建议不要使用SINGLETON以外的方式,因为其他两种方式多多少少会增加系统消耗,影响到程序的性能。
总结
关于NestJS依赖注入相关的内容已经介绍的差不多了,有了这些基础,相信你可以在这块能比较顺利的开展工作了。如果你在使用的过程中遇到什么问题,可以通过翻阅官方文档了解更多细节。
在后面的一两篇文章内,我将计划再介绍一些关于NestJS框架的其他核心基础,我一直相信,基础打好了,才会让你往后的做事效率达到事半功倍的效果。