Node.js服务端开发教程 (五):依赖注入进阶篇

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

在前一篇文章《依赖注入基础篇》中,我们了解了依赖注入和控制反转的基本概念,大致知道它是怎么一回事。并通过简单的例子,学习到了在NestJS框架下如何使用依赖注入功能。今天,我们需要再多花点时间,进一步了解更多关于使用NestJS依赖注入的功能细节。

在使用了依赖注入功能的程序中,我们可以从资源的角度,把代码中的对象角色分为以下3种:

  • 容器 - 是所有资源的管理者。程序中可被注入的资源都由容器来发起创建和维护其生命周期
  • 资源提供者 - 资源创建的实际执行者。所有的资源提供者都需要在容器进行注册登记,然后由容器来进行统一调度
  • 资源使用者 - 就是那些需要使用到容器中管理的那些资源的消费者了

有些情况下,资源提供者本身即是提供者也是使用者。记住一点,只要依赖于其他资源的对象,它就是一个资源使用者。

资源提供者

在NestJS框架中,基础类型值、对象、函数等,都可以被作为资源来使用。在代码中要使用这些资源,需要经过一种中间者来创建和提供:资源提供者(Providers)。

NestJS中的资源提供者主要分为4种类型:

第一种类型,是使用类作为提供者,称为ClassProvider。它也是我们日常开发中会最经常用到的一种资源提供者。一个普通的类,通过添加 @Injectable 装饰器,就可以成为一个资源提供者。

我们之前提到过,资源提供者是需要先经过注册之后才能被容器所使用。资源提供者的注册工作是在模块(Module)中进行的。让我们打开命令行,进入到NestJS项目的目录下,执行命令:

nest g module product

该命令是NestJS命令行工具提供的代码生成器功能,可以帮我们快速生成一个模块(Module)代码文件。成功执行后,你可以看到项目的src目录下多了一个product子目录,且下面生成了一个名为product.module.ts的模块代码文件。接着再执行命令:

nest g service product

product目录下又生成了一个名为product.service.ts的文件,以及一个同名的spec文件,前者就是一个典型的类资源提供者,后者是它对应的单元测试类。现在这个资源提供者类还是空的,没有什么具体的功能,让我们往这个类里添加一个方法函数:

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

@Injectable()
export class ProductService {
  getProducts(): string[] {
    return [
      'iPhone 11', 
      'iPhone 11 pro', 
      'iPhone 11 pro max'
    ];
  }
}

另外,我们发现 product.module.ts 文件也被自动更新过了,新生成的ProductService类被自动注册进了 product.module.ts 所代表的模块中:

import { Module } from '@nestjs/common';
import { ProductService } from './product.service';

@Module({
  providers: [ProductService]
})
export class ProductModule {}

以上这种将一个由 @Injectable 装饰器处理过的类配置到模块装饰器 @Module 的参数选项 providers 中的过程,即完成了对类资源提供者的注册工作。

其实上面的这种是简写形式,完整的写法是这样的:

import { Module } from '@nestjs/common';
import { ProductService } from './product.service';

@Module({
  providers: [
    {
      provide: ProductService,
      useClass: ProductService
    }
  ]
})
export class ProductModule {}

provide 属性被称为注入令牌(Injection Token),它类似于像在Map中存储值时的key,让容器在执行对资源依赖方注入需要的资源时,可以正确查找匹配到容器中的资源实例。注入令牌可以使用多种类型的值:string、symbol、类、抽象类、函数都可以作为令牌值使用。比如:

{
  provide: 'myProductService',
  useClass: ProductService
}

useClass 则用于指定生成资源实例的类。

第二种类型,是使用常量值(可以是简单基础类型值,也可以是对象),称为ValueProvider。它非常适用于做配置性的工作,或者是Mock测试。

我们可以在前面的ProductModule中添加一个常量资源提供者的注册:

import { Module } from '@nestjs/common';
import { ProductService } from './product.service';

@Module({
  providers: [
    ProductService,
    {
      provide: 'AUTHOR_NAME',
      useValue: '一斤代码'
    }
  ]
})
export class ProductModule {}

这种常量提供者通常用来为程序提供一些不太变化的配置信息。

另外,由于上述方式具有可直接提供一个值或对象的特点,它可被用来做Mock测试。试想一下场景:你原先的真实代码需要查询数据库,但是在做单元测试的时候,真的要去查库会比较不方便,你希望你的代码里返回你设计好的固定测试数据就好了。可能你也有这种经历,通常你是不是会去修改原先的代码,注释掉查库操作,然后输出一些固定值?当然,这种做法是可行的,但是,这种通过修改原先业务代码的方式,是不可取的。

在NestJS中,我们可以采用这样的做法,以实现对原先业务逻辑非破坏性的Mock替换:

import { Module } from '@nestjs/common';
import { ProductService } from './product.service';

// Mock对象
const myProductService = {
  getProducts() {
    return [
      'iPhone 4',
      'iPhone 4s',
    ];
  }
}

@Module({
  providers: [
    {
      provide: ProductService,
      // 使用Mock对象来替代原先通过ProductService类生成的对象
      useValue: myProductService
    }
  ]
})
export class ProductModule { }

第三种类型,是使用工厂函数,称为FactoryProvider。它适用于需要更为动态的创建资源的场景。

比如,我们将上文中的ProductService改一下,增加一个构造函数参数:

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

@Injectable()
export class ProductService {
    // 构造函数,接受一个 author参数
    constructor(private readonly author: string) {}
    getProducts(): string[] {
        return [
            'iPhone 11',
            'iPhone 11 pro',
            'iPhone 11 pro max',
        ];
    }
}

改造后,要实例化这个类的话,就需要在实例化时传参。针对这种情况,NestJS提供了这样的写法:

import { Module } from '@nestjs/common';
import { ProductService } from './product.service';

@Module({
  providers: [
    {
      provide: 'AUTHOR_NAME',
      useValue: '一斤代码',
    },
    {
      provide: ProductService,
      // 工厂函数
      useFactory: (author: string) => {
        return new ProductService(author);
      },
      // 注入其他资源作为工厂函数参数
      inject: ['AUTHOR_NAME']
    },
  ],
  exports: [ProductService]
})
export class ProductModule { }

第四种类型,其实是一种用于给其他已有的资源提供者创建其他别名的方式,称为ExistingProvider。

这个还是比较简单的,使用useExisting指定源提供者就可以了:

import { Module } from '@nestjs/common';
import { ProductService } from './product.service';

@Module({
  providers: [
    // 一个class类提供者
    ProductService,
    // 上面的提供者的别名
    {
      provide: 'AliasedProductService',
      useExisting: ProductService
    }
  ]
})
export class ProductModule { }

资源注入方式

上面讲解了4种资源提供者,它们负责资源的创建。现在我们来说说资源的使用。在依赖注入框架中,资源通过容器的调度,被注入到资源使用者内。在NestJS中,我们的资源使用者都是以类的形式存在的,所以资源的注入方式存在以下2种可能:

  • 通过类的构造函数注入
  • 通过类的属性注入

通过构造函数的方式可能是平时开发中最常用的。我们为需要注入资源的类编写构造函数,并列出需要注入的资源即可:

@Injectable()
export class CategoryService {
  constructor(private readonly productService: ProductService) { }
}

如果资源的注入令牌不是class类型的,则需要显式的使用 @Inject 装饰器来指定:

@Injectable()
export class CategoryService {
  constructor(
    @Inject('myProductService')
    private readonly productService: ProductService) { }
}

而通过属性的注入方式是另一种可选途径。

@Injectable()
export class CategoryService {
  @Inject('myProductService')
  private readonly productService: ProductService;
}

值得注意的是,当你的代码中指定了资源注入,而容器中却并没有相应资源的时候,程序会报错。但有时候你的代码期望这样工作:如果程序中提供了配置信息,则使用该配置信息,否则使用默认配置信息。这种情况下,作为注入资源的配置信息显然是可选的,即使没有,程序也不该出错。NestJS当然考虑到了这一点,它提供了 @Optional 装饰器来满足上述场景:

@Injectable()
export class CategoryService {
  constructor(
    @Optional()
    @Inject('myProductService')
    private readonly productService: ProductService) { }
}

总结

关于资源提供者和资源注入方式的相关知识,今天就先介绍到这里吧。这些内容都非常的重要,需要好好的理解消化一下,因为依赖注入是NestJS的核心。后面还遗留下一些诸如异步资源提供者、循环依赖、注入范围等知识点,待后面再一起探讨吧。

关注首发公众号:默碟

推荐阅读更多精彩内容