在前一篇文章《依赖注入基础篇》中,我们了解了依赖注入和控制反转的基本概念,大致知道它是怎么一回事。并通过简单的例子,学习到了在NestJS框架下如何使用依赖注入功能。今天,我们需要再多花点时间,进一步了解更多关于使用NestJS依赖注入的功能细节。
在使用了依赖注入功能的程序中,我们可以从资源的角度,把代码中的对象角色分为以下3种:
- 容器 - 是所有资源的管理者。程序中可被注入的资源都由容器来发起创建和维护其生命周期
- 资源提供者 - 资源创建的实际执行者。所有的资源提供者都需要在容器进行注册登记,然后由容器来进行统一调度
- 资源使用者 - 就是那些需要使用到容器中管理的那些资源的消费者了
有些情况下,资源提供者本身即是提供者也是使用者。记住一点,只要依赖于其他资源的对象,它就是一个资源使用者。
资源提供者
在NestJS框架中,基础类型值、对象、函数等,都可以被作为资源来使用。在代码中要使用这些资源,需要经过一种中间者来创建和提供:资源提供者(Providers)。
NestJS中的资源提供者主要分为4种类型:
第一种类型,是使用类作为提供者,称为ClassProvider。它也是我们日常开发中会最经常用到的一种资源提供者。一个普通的类,通过添加 @Inectable 装饰器,就可以成为一个资源提供者。
我们之前提到过,资源提供者是需要先经过注册之后才能被容器所使用。资源提供者的注册工作是在模块(Module)中进行的。让我们打开命令行,进入到NestJS项目的目录下,执行命令:
代码语言:javascript复制nest g module product
该命令是NestJS命令行工具提供的代码生成器功能,可以帮我们快速生成一个模块(Module)代码文件。成功执行后,你可以看到项目的src目录下多了一个product子目录,且下面生成了一个名为product.module.ts的模块代码文件。接着再执行命令:
代码语言:javascript复制nest g service product
product目录下又生成了一个名为product.service.ts的文件,以及一个同名的spec文件,前者就是一个典型的类资源提供者,后者是它对应的单元测试类。现在这个资源提供者类还是空的,没有什么具体的功能,让我们往这个类里添加一个方法函数:
代码语言:javascript复制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 所代表的模块中:
代码语言:javascript复制import { Module } from '@nestjs/common';
import { ProductService } from './product.service';
@Module({
providers: [ProductService]
})
export class ProductModule {}
以上这种将一个由 @Injectable 装饰器处理过的类配置到模块装饰器 @Module 的参数选项 providers 中的过程,即完成了对类资源提供者的注册工作。
其实上面的这种是简写形式,完整的写法是这样的:
代码语言:javascript复制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中添加一个常量资源提供者的注册:
代码语言:javascript复制import { Module } from '@nestjs/common';
import { ProductService } from './product.service';
@Module({
providers: [
ProductService,
{
provide: 'AUTHOR_NAME',
useValue: '一斤代码'
}
]
})
export class ProductModule {}
这种常量提供者通常用来为程序提供一些不太变化的配置信息。
另外,由于上述方式具有可直接提供一个值或对象的特点,它可被用来做Mock测试。试想一下场景:你原先的真实代码需要查询数据库,但是在做单元测试的时候,真的要去查库会比较不方便,你希望你的代码里返回你设计好的固定测试数据就好了。可能你也有这种经历,通常你是不是会去修改原先的代码,注释掉查库操作,然后输出一些固定值?当然,这种做法是可行的,但是,这种通过修改原先业务代码的方式,是不可取的。
在NestJS中,我们可以采用这样的做法,以实现对原先业务逻辑非破坏性的Mock替换:
代码语言:javascript复制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改一下,增加一个构造函数参数:
代码语言:javascript复制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提供了这样的写法:
代码语言:javascript复制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指定源提供者就可以了:
代码语言:javascript复制import { Module } from '@nestjs/common';
import { ProductService } from './product.service';
@Module({
providers: [
// 一个class类提供者
ProductService,
// 上面的提供者的别名
{
provide: 'AliasedProductService',
useExisting: ProductService
}
]
})
export class ProductModule { }
资源注入方式
上面讲解了4种资源提供者,它们负责资源的创建。现在我们来说说资源的使用。在依赖注入框架中,资源通过容器的调度,被注入到资源使用者内。在NestJS中,我们的资源使用者都是以类的形式存在的,所以资源的注入方式存在以下2种可能:
- 通过类的构造函数注入
- 通过类的属性注入
通过构造函数的方式可能是平时开发中最常用的。我们为需要注入资源的类编写构造函数,并列出需要注入的资源即可:
代码语言:javascript复制@Injectable()
export class CategoryService {
constructor(private readonly productService: ProductService) { }
}
如果资源的注入令牌不是class类型的,则需要显式的使用 @Inject 装饰器来指定:
代码语言:javascript复制@Injectable()
export class CategoryService {
constructor(
@Inject('myProductService')
private readonly productService: ProductService) { }
}
而通过属性的注入方式是另一种可选途径。
代码语言:javascript复制@Injectable()
export class CategoryService {
@Inject('myProductService')
private readonly productService: ProductService;
}
值得注意的是,当你的代码中指定了资源注入,而容器中却并没有相应资源的时候,程序会报错。但有时候你的代码期望这样工作:如果程序中提供了配置信息,则使用该配置信息,否则使用默认配置信息。这种情况下,作为注入资源的配置信息显然是可选的,即使没有,程序也不该出错。NestJS当然考虑到了这一点,它提供了 @Optional 装饰器来满足上述场景:
代码语言:javascript复制@Injectable()
export class CategoryService {
constructor(
@Optional()
@Inject('myProductService')
private readonly productService: ProductService) { }
}
总结
关于资源提供者和资源注入方式的相关知识,今天就先介绍到这里吧。这些内容都非常的重要,需要好好的理解消化一下,因为依赖注入是NestJS的核心。后面还遗留下一些诸如异步资源提供者、循环依赖、注入范围等知识点,待后面再一起探讨吧。