Node.js服务端开发教程 (六):依赖注入补漏篇

最新推荐:《Vue3.0抢先学》系列学习教程

最近在写前面两篇关于依赖注入的文章时,我总是在想用一句怎么的话来简单而朴素的描述依赖注入的概念,让从来没接触过的朋友能比较形象的去理解。想来想去,觉得可以站在依赖注入容器的角度说:

你负责告诉我你需要什么(依赖),我负责给你送来什么(注入)

建议多读几遍上面这句话,再回头去阅读前面两篇文章,我觉得你会有更多的收获。

其实在前两篇文章中,关于NestJS依赖注入功能相关的内容已经介绍的差不多了,如果你掌握了的话,已可以顺利的用于实际的开发工作。今天想给大家介绍的是一些关于依赖注入的零碎遗留内容,在日常开发中也会遇到,但不是那么高频。主要有以下几点:

  • 异步资源提供者
  • 循环依赖问题与解决方式
  • 注入范围

异步资源提供者

顾名思义,其实就是在资源创建的时候,存在异步的环节。比如在创建资源的时候,需要先访问一个后端API来获取一些配置信息,然后根据这些配置信息再做进一步的资源创建。这里的后端API访问就是一个异步的动作,这会导致整个资源创建流程也是异步的了。

在NestJS中,大多数的资源提供者都是只支持同步,比如ValueProvider和ClassProvider,能支持异步的只有FactoryProvider。用法其实挺简单的:

{
  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)的技术来解析循环依赖项。

例如下面示例代码:

@Injectable()
export class CategoryService {
  constructor(
    @Inject(forwardRef(() => ProductService))
    private readonly productService: ProductService,
) {}
}
@Injectable()
export class ProductService {
  constructor(
    @Inject(forwardRef(() => CategoryService))
    private readonly categoryService: CategoryService,
) {}
}

以上的2个类之间有互相依赖关系,各自需要注入对方。如果未使用代码中NestJS框架提供的forwardRef()工具函数,就会报错提示找不到依赖的资源;而使用后,容器可以正确处理互相使用forwardRef()函数标记过的类。

该工具函数也可作用于2个模块之间,解决模块间的循环依赖:

@Module({
  imports: [forwardRef(() => CategoryModule)],
})
export class ProductModule {}
@Module({
  imports: [forwardRef(() => ProductModule)],
})
export class CategoryModule {}

除了使用上面提到的 forwardRef() 工具函数,NestJS还另外提供了一种可行的方式来解决循环依赖,那就是模块引用(Module Reference)。模块引用解决问题的思路是:不通过容器的自动依赖注入,而由我们自己来控制。

通过在类中注入框架提供的ModuleRef,并在模块初始化的生命周期函数中进行手动查找所需要的资源实例,就能避免自动注入时的尴尬问题:

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装饰器中指定范围:

import { Injectable, Scope } from '@nestjs/common';

@Injectable({ scope: Scope.REQUEST })
export class MyService {}

也可以在定义资源提供者的地方指定范围:

{
  provide: 'MY_MANAGER',
  useClass: MyManager,
  scope: Scope.TRANSIENT,
}

另外,资源依赖路径上的范围会有层级关系,是一个从底至上的冒泡关系,比如下面这样一个A依赖B,B依赖C的关系中:

AService <- BService <- CService

如果我们指定BService的范围为REQUEST,那么上层的AService也会变成REQUEST的,而下层的CService则仍保持默认的SINGLETON。

如果没有特别的原因,建议不要使用SINGLETON以外的方式,因为其他两种方式多多少少会增加系统消耗,影响到程序的性能。

总结

关于NestJS依赖注入相关的内容已经介绍的差不多了,有了这些基础,相信你可以在这块能比较顺利的开展工作了。如果你在使用的过程中遇到什么问题,可以通过翻阅官方文档了解更多细节。

在后面的一两篇文章内,我将计划再介绍一些关于NestJS框架的其他核心基础,我一直相信,基础打好了,才会让你往后的做事效率达到事半功倍的效果。

关注首发公众号:默碟

推荐阅读更多精彩内容