本文已参与「新人创作礼」活动,一起开启掘金创作之路。
依赖注入基础
Dependency injection(依赖注入)是一种实现控制反转(Inversion of control)的技术,将依赖项的实例化工作交给了IoC容器(NestJS运行时系统),而不是我们自己通过代码命令式地去完成它。
提供者
在Nest中,很多基础的Nest类都被当作是提供者(Provider),比如:service,repositories,factories,helpers等。
提供者是在一个模块(module)中作为providers声明的JavaScript类。
提供者的核心在于它能够作为依赖被注入。
提供者的使用步骤:
- 定义提供者;
- 在需要使用到的地方进入注入;
- 在Nest容器中进行注册;
标准提供者
在@Module()装饰器使用时,我们能以这样的方式传入提供者:
@Module({
providers: [AppService]
})
export class AppModule {}
它其实是下面语法的简写:
@Module({
providers: [
{
provide: AppService, // token用于请求具有相同名称的类的实例
useClass: AppService
}
]
})
export class AppModule {}
自定义提供者
Nest提供给了我们一些方式去自定义提供者
1. 值提供者
通过useValue语法来我们可以用来注入一个常量,在Nest容器中加入一个外部库或者用一个模拟对象来取代实际实现。
例如:使用mockAppService来代替AppService
const mockAppService = {
getHello() {
return '这是mockAppService中的getHello';
},
};
@Module({
imports: [],
controllers: [AppController],
providers: [
{
provide: AppService,
useValue: mockAppService,
},
],
})
export class AppModule {}
在上面的这个例子中,我们使用了AppService这个类名作为提供者的token,而将mockAppService作为它解析的值。在进行依赖注入的时候,我们采用基于构造器的方式(标准模式)进行注入,并且在构造器中使用的token同样是一个类名,这样它们就会匹配上。
constructor(private readonly appService: AppService) {}
2. 不基于类名的提供者token
有时,我们希望能够更加灵活地采用字符串或者symbo来表示依赖注入的token,比如:
const mockAppService = {
getHello() {
return '这是mockAppService中的getHello';
},
};
@Module({
imports: [],
controllers: [AppController],
providers: [
// AppService
// {
// provide: AppService,
// useValue: mockAppService,
// },
{
provide: 'APP_SERVICE',
useValue: mockAppService,
},
]
})
export class AppModule {}
这种模式的自定义提供者使用了字符串作为token,那么在进行注入的时候,我们需要使用到@Inject()装饰器,它接收一个参数作为token使用。
3. 类提供者
使用useClass语法使得我们能够动态的去决定一个提供者的token应该解析成哪一个类。
比如:我们有一个抽象类ConfigService,我们希望Nest容器能够根据当前环境来来提供这个ConfigService不同的实现类:
export abstract class ConfigServcie {
abstract getConfig(): void;
}
export class DevConfigServcie extends ConfigServcie {
getConfig() {
console.log('加载开发环境配置');
}
}
export class ProdConfigService extends ConfigServcie {
getConfig(): void {
console.log('加载生成环境配置');
}
}
const environment = 'dev';
@Module({
imports: [],
controllers: [AppController],
providers: [
{
provide: ConfigServcie,
useClass: environment === 'dev' ? DevConfigServcie : ProdConfigService,
},
]
})
export class AppModule {}
4. 工厂提供者
使用useFactory语法能够动态地创建提供者,实际使用的提供者是通过一个工厂函数返回的:
import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import {
ConfigServcie,
DevConfigServcie,
ProdConfigService,
} from './class/config.class';
const environment = 'dev';
@Module({
imports: [],
controllers: [AppController],
providers: [
{
provide: ConfigServcie,
useFactory: () => {
return environment === 'dev'
? new DevConfigServcie()
: new ProdConfigService();
},
},
],
})
export class AppModule {}
工厂函数还可以注入其它的提供者,它通过下面的方式来使用:
- 工厂函数接收参数;
- 在inject选项中接收需要注入到工厂函数的提供者,它们将会在Nest容器进行实例化处理时被解析并作为参数传递到工厂函数中,同时这个提供者可以被标记为可选的。
注意:inject选项中的提供者需要跟工厂函数中接收的参数列表顺序一致
例如下面的例子:我们希望能够根据不同的环境来连接不同的数据库:
import { Injectable } from '@nestjs/common';
@Injectable()
export class Options {
get(dbName: string) {
return `mongodb://${dbName}`;
}
}
import { Injectable } from '@nestjs/common';
@Injectable()
export class Connection {
constructor(private readonly dbUrl: string) {}
connect() {
console.log(`通过${this.dbUrl}进行数据库连接`);
}
}
import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { Options } from './provider/options';
import { Connection } from './provider/connection';
const environment = 'dev';
@Module({
imports: [],
controllers: [AppController],
providers: [
AppService,
{
provide: Connection,
useFactory: (options: Options) => {
const dbUrl =
environment === 'dev'
? options.get('测试数据库')
: options.get('正式数据库');
return new Connection(dbUrl);
},
inject: [Options],
},
Options,
],
})
export class AppModule {}
import { Controller, Get } from '@nestjs/common';
import { AppService } from './app.service';
@Controller()
export class AppController {
constructor(
private readonly appService: AppService,
) {}
@Get()
getHello(): string {
return this.appService.getHello();
}
}
import { Injectable } from '@nestjs/common';
import { Connection } from './provider/connection';
@Injectable()
export class AppService {
constructor(private readonly connection: Connection) {}
getHello(): string {
this.connection.connect();
return 'Hello World!';
}
}
5. 别名提供者
通过useExisting语法我们可以给已经注册过的提供者创建一个别名,使得我们可以通过两种方式来访问同一个提供者。如果它们的作用域设置的都是单例(SINGLETION),那么它们将被解析成同一个实例。
6. 不基于服务的提供者
虽然提供者通常用于提供服务,但是它们的用途不仅限于此。
提供者可以提供任何的值,比方说它可以基于当前的环境来提供一个配置对象数组。
导出自定义提供者
跟普通的提供者一样,自定义提供者的作用域只在它所声明的模块中。如果想要被其它模块访问,那么它必须要被导出。
导出的方式,我们既可以导出提供者的token也可以使用完整的提供者对象:
@Module({
imports: [UserModule],
controllers: [AppController],
providers: [
AppService,
{
provide: Connection,
useFactory: (options: Options) => {
const dbUrl =
environment === 'dev'
? options.get('测试数据库')
: options.get('正式数据库');
return new Connection(dbUrl);
},
inject: [Options],
},
Options,
],
exports: [
AppService,
{
provide: Connection,
useClass: Connection,
},
],
})
export class AppModule {}