基本原理
# 01. 自定义提供者
DI: 依赖注入
# DI的基本原理
依赖注入是一种反转控制(IoC)技术,将依赖的实例化委托给IoC容器(在我们的例子中,NestJS运行时系统),而不是在你自己的代码中强制进行。让我们来看看在Providers章节的这个例子中发生了什么。
首先,我们定义一个提供者。@Injectable()
装饰器将 CatsService 类标记为提供者。
// cats.service.ts
import { Injectable } from '@nestjs/common';
import { Cat } from './interfaces/cat.interface';
@Injectable()
export class CatsService {
private readonly cats: Cat[] = [];
findAll(): Cat[] {
return this.cats;
}
}
然后,我们要求 Nest 将提供程序注入到我们的控制器类中
// cats.controller.tsJS
import { Controller, Get } from '@nestjs/common';
import { CatsService } from './cats.service';
import { Cat } from './interfaces/cat.interface';
@Controller('cats')
export class CatsController {
constructor(private catsService: CatsService) {}
@Get()
async findAll(): Promise<Cat[]> {
return this.catsService.findAll();
}
}
最后,我们在 Nest IoC 容器中注册提供程序
// cats.module.tsJS
import { Module } from '@nestjs/common';
import { CatsController } from './cats/cats.controller';
import { CatsService } from './cats/cats.service';
@Module({
controllers: [CatsController],
providers: [CatsService],
})
export class AppModule {}
这个过程有三个关键步骤:
在 cats.service.ts 中
@Injectable()
装饰器声明 CatsService 类是一个可以由Nest IoC容器管理的类。在 cats.controller.ts 中 CatsController 声明了一个依赖于 CatsService 令牌(token)的构造函数注入:
constructor(private catsService: CatsService)
- 在 app.module.ts 中,我们将标记 CatsService与 cats.service.ts文件中的 CatsService 类相关联。 我们将在下面确切地看到这种关联(也称为注册)的发生方式。
当Nest IoC容器实例化一个CatsController时,它首先寻找任何依赖关系*。当它找到CatsService依赖时,它会对CatsService标记进行查找,根据注册步骤3 返回CatsService类。假设是SINGLETON范围(默认行为),Nest将创建一个CatsService的实例,将其缓存并返回,或者如果已经缓存了一个,则返回现有实例。
上面这个解释稍微简化了一点。我们忽略的一个重要方面是,分析依赖项代码的过程非常复杂,并且发生在应用程序引导期间。一个关键特性是依赖关系分析(或“创建依赖关系图”)是可传递的。 在上面的示例中,如果 CatsService 本身具有依赖项,那么那些依赖项也将得到解决。 依赖关系图确保以正确的顺序解决依赖关系-本质上是自下而上
。 这种机制使开发人员不必管理此类复杂的依赖关系图。
# 标准提供者
让我们仔细看看 @Module()装饰器。在app.module中,我们声明:
@Module({
controllers: [CatsController],
providers: [CatsService],
})
providers属性接受一个提供者数组。到目前为止,我们已经通过一个类名列表提供了这些提供者。实际上,该语法providers: [CatsService]是更完整语法的简写:
providers: [
{
provide: CatsService,
useClass: CatsService,
},
];
现在我们看到了这个显式的构造,我们可以理解注册过程。在这里,我们明确地将令牌 CatsService与类 CatsService 关联起来。简写表示法只是为了简化最常见的用例,其中令牌用于请求同名类的实例。
# 自定义提供者
当您的要求超出标准提供商所提供的要求时,会发生什么?这里有一些例子:
您要创建自定义实例,而不是让 Nest 实例化(或返回其缓存实例)类 您想在第二个依赖项中重用现有的类 您想使用模拟版本覆盖类进行测试。
Nest允许你定义自定义提供者来处理这些情况。它提供了几种方法来定义自定义提供者。
# 值提供者 (useValue)
useValue
语法对于注入常量值、将外部库放入 Nest 容器或使用模拟对象替换实际实现非常有用。
import { CatsService } from './cats.service';
const mockCatsService = {
/* mock implementation
...
*/
};
@Module({
imports: [CatsModule],
providers: [
{
provide: CatsService,
useValue: mockCatsService,
},
],
})
export class AppModule {}
在这个例子中,CatsService标记将解析为mockCatsService模拟对象。 useValue需要一个值--在这个例子中是一个具有与它所取代的CatsService类相同接口的文字对象。
# 非类提供者
到目前为止,我们已经使用了类名作为我们的提供者标记( providers 数组中列出的提供者中的 Provide 属性的值)。 这与基于构造函数的注入所使用的标准模式相匹配,其中令牌也是类名。
import { connection } from './connection';
@Module({
providers: [
{
provide: 'CONNECTION',
useValue: connection,
},
],
})
export class AppModule {}
在本例中,我们将字符串值令牌('CONNECTION')与从外部文件导入的已存在的连接对象相关联。
@Injectable()
export class CatsRepository {
constructor(@Inject('CONNECTION') connection: Connection) {}
}
# 类提供者:useClass
useClass语法允许你动态地确定一个令牌应该解析到的类。例如,假设我们有一个抽象的(或默认的)ConfigService类。根据当前的环境,我们希望Nest能提供不同的配置服务的实现。下面的代码实现了这样一个策略。
const configServiceProvider = {
provide: ConfigService,
useClass:
process.env.NODE_ENV === 'development'
? DevelopmentConfigService
: ProductionConfigService,
};
@Module({
providers: [configServiceProvider],
})
export class AppModule {}
我们首先定义对象 configServiceProvider,然后将其传递给模块装饰器的 providers 属性。 这只是一些代码组织,但是在功能上等同于我们到目前为止在本章中使用的示例。
另外,我们使用 ConfigService 类名称作为令牌。 对于任何依赖 ConfigService 的类,Nest 都会注入提供的类的实例( DevelopmentConfigService 或 ProductionConfigService),该实例将覆盖在其他地方已声明的任何默认实现(例如,使用 @Injectable() 装饰器声明的 ConfigService)。
# 工厂提供者(useFactory)
useFactory
语法允许动态地创建提供者。实际的提供者将由工厂函数返回的值提供。工厂函数可以根据需要变得简单或复杂。一个简单的工厂可以不依赖于任何其它提供者。一个更复杂的工厂可以自己注入它所需要的其他提供者来计算其结果。对于后一种情况,工厂提供者语法有一对相关机制。
- 工厂函数可以接受(可选)参数。
- (可选的)
inject
属性接受一个提供者数组,Nest将在实例化过程中解析并作为参数传递给工厂函数。另外,这些提供者可以被标记为可选。这两个列表应该是相互关联的。Nest将以相同的顺序将inject列表中的实例作为参数传递给工厂函数。下面的例子演示了这一点。
const connectionFactory = {
provide: 'CONNECTION',
useFactory: (optionsProvider: OptionsProvider, optionalProvider?: string) => {
const options = optionsProvider.get();
return new DatabaseConnection(options);
},
inject: [OptionsProvider, { token: 'SomeOptionalProvider', optional: true }],
// \_____________/ \__________________/
// This provider The provider with this
// is mandatory. token can resolves to `undefined`.
};
@Module({
providers: [
connectionFactory,
OptionsProvider,
// { provide: 'SomeOptionalProvider', useValue: 'anything' },
],
})
export class AppModule {}
# 别名提供者 (useExisting)
useExisting 语法允许您为现有的提供程序创建别名。这将创建两种访问同一提供者的方法。在下面的示例中,(基于string)令牌 'AliasedLoggerService' 是(基于类的)令牌 LoggerService 的别名。假设我们有两个不同的依赖项,一个用于 'AlilasedLoggerService' ,另一个用于 LoggerService 。如果两个依赖项都用单例作用域指定,它们将解析为同一个实例。
@Injectable()
class LoggerService {
/* implementation details */
}
const loggerAliasProvider = {
provide: 'AliasedLoggerService',
useExisting: LoggerService,
};
@Module({
providers: [LoggerService, loggerAliasProvider],
})
export class AppModule {}
# 非服务提供者
虽然提供者经常提供服务,但他们并不局限于这种用途。一个提供者可以提供任何价值。例如,提供者可以根据当前环境提供一个配置对象的数组,如下所示。
const configFactory = {
provide: 'CONFIG',
useFactory: () => {
return process.env.NODE_ENV === 'development' ? devConfig : prodConfig;
},
};
@Module({
providers: [configFactory],
})
export class AppModule {}
# 导出自定义提供者
与任何提供程序一样,自定义提供程序的作用域仅限于其声明模块。要使它对其他模块可见,必须导出它。要导出自定义提供程序,我们可以使用其令牌或完整的提供程序对象。
const connectionFactory = {
provide: 'CONNECTION',
useFactory: (optionsProvider: OptionsProvider) => {
const options = optionsProvider.get();
return new DatabaseConnection(options);
},
inject: [OptionsProvider],
};
@Module({
providers: [connectionFactory],
exports: ['CONNECTION'],
})
export class AppModule {}
使用整个对象:
const connectionFactory = {
provide: 'CONNECTION',
useFactory: (optionsProvider: OptionsProvider) => {
const options = optionsProvider.get();
return new DatabaseConnection(options);
},
inject: [OptionsProvider],
};
@Module({
providers: [connectionFactory],
exports: [connectionFactory],
})
export class AppModule {}
# 03. 异步提供者
有时,应用程序的启动应该被推迟到一个或多个异步任务完成之后。例如,你可能不希望在与数据库建立连接之前开始接受请求。你可以使用异步提供者来实现这一点。
其语法是使用 useFactory
语法的 async/await
。工厂返回一个承诺,工厂函数可以等待异步任务。在实例化依赖于(注入)这样一个提供程序的任何类之前,Nest将等待承诺的解决。
{
provide: 'ASYNC_CONNECTION',
useFactory: async () => {
const connection = await createConnection(options);
return connection;
},
}
# 注入
与任何其他提供程序一样,异步提供程序通过其令牌被注入到其他组件。在上面的示例中,您将使用结构@Inject('ASYNC_CONNECTION')
。
# 03. 动态模块
文档中 "概述 "部分的大多数应用代码示例都使用了常规或静态的模块。 模块定义了像提供者和控制器这样的组件组,它们作为一个整体应用程序的模块化部分被组合在一起。它们为这些组件提供了一个执行环境,或范围。例如,模块中定义的提供者对模块的其他成员是可见的,不需要导出它们。当一个提供者需要在模块外可见时,它首先要从其宿主模块中导出,然后再导入到其消费模块中。
定义一个 UsersModule
来提供和导出 UsersService。UsersModule是 UsersService的主机模块。
import { Module } from '@nestjs/common';
import { UsersService } from './users.service';
@Module({
providers: [UsersService],
exports: [UsersService],
})
export class UsersModule {}
然后,我们定义一个 AuthModule
,它导入 UsersModule
,使 UsersModule导出的提供程序在 AuthModule中可用:
import { Module } from '@nestjs/common';
import { AuthService } from './auth.service';
import { UsersModule } from '../users/users.module';
@Module({
imports: [UserModule],
providers: [AuthService],
exports: [AuthService]
})
export class AuthModule {}
这些构造使我们能够注入 UsersService 例如 AuthService 托管在其中的 AuthModule:
import { Injectable } from '@nestjs/common';
import { UsersService } from '../users/users.service';
@Injectable()
export class AuthService {
constructor(private readonly usersService: UsersService) {}
/*
Implementation that makes use of this.usersService
*/
}
我们将其称为静态模块绑定。Nest在主模块和消费模块中已经声明了连接模块所需的所有信息。让我们来看看这个过程中发生了什么。Nest通过以下方式使 UsersService
在AuthModule
中可用:
实例化
UsersModule
,包括传递导入UsersModule
本身使用的其他模块,以及传递的任何依赖项(参见自定义提供程序)。实例化
AuthModule
,并将UsersModule
导出的提供者提供给AuthModule
中的组件(就像在AuthModule
中声明它们一样)。在
AuthService
中注入UsersService
实例。
# 动态模块实例
使用静态模块绑定,消费模块不会影响来自主机模块的提供者的配置方式。为什么这很重要?考虑这样一种情况:我们有一个通用模块,它需要在不同的用例中有不同的行为。这类似于许多系统中的插件概念,在这些系统中,其中一个通用功能在被消费者使用之前需要一些配置。
Nest
的一个很好的例子是配置模块。 许多应用程序发现使用配置模块来外部化配置详细信息很有用。 这使得在不同部署中动态更改应用程序设置变得容易:例如,开发人员的开发数据库,测试环境的数据库等。通过将配置参数的管理委派给配置模块,应用程序源代码保持独立于配置参数。
主要在于配置模块本身,因为它是通用的(类似于 '插件' ),需要由它的消费模块进行定制。这就是动态模块发挥作用的地方。使用动态模块特性,我们可以使配置模块成为动态的,这样消费模块就可以使用 API
来控制配置模块在导入时是如何定制的。
换句话说,动态模块提供了一个 API
,用于将一个模块导入到另一个模块中,并在导入模块时定制该模块的属性和行为,而不是使用我们迄今为止看到的静态绑定。
# 配置模块示例
在不同项目中使用 ConfigModule
时,选择对应的文件夹
动态模块使我们能够将参数传递到要导入的模块中,以便我们可以更改其行为。
静态导入 ConfigModule 的示例(即,一种无法影响导入模块行为的方法)
import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { ConfigModule } from './config/config.module';
@Module({
imports: [ConfigModule],
controllers: [AppController],
providers: [AppService],
})
export class AppModule {}
动态模块导入
import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { ConfigModule } from './config/config.module';
@Module({
imports: [ConfigModule.register({ folder: './config' })],
controllers: [AppController],
providers: [AppService],
})
export class AppModule {}
上面的动态示例中变化的部分是什么?
ConfigModule
是一个普通类,因此我们可以推断它必须有一个名为register()
的静态方法。我们知道它是静态的,因为我们是在ConfigModule
类上调用它,而不是在类的实例上。注意:我们将很快创建的这个方法可以有任意名称,但是按照惯例,我们应该调用它forRoot()
或register()
方法。register()
方法是由我们定义的,因此我们可以接受任何我们喜欢的参数。在本例中,我们将接受具有适当属性的简单 options 对象,这是典型的情况。我们可以推断 register() 方法必须返回类似模块的内容,因为它的返回值出现在熟悉的导入列表中,到目前为止,我们已经看到该列表包含了一个模块列表。
实际上,我们的 register()
方法将返回的是 DynamicModule。
动态模块无非就是在运行时创建的模块,它具有与静态模块相同属性,外加一个称为模块的附加属性。
静态模块声明:
@Module({
imports: [DogsService],
controllers: [CatsController],
providers: [CatsService],
exports: [CatsService]
})
动态模块必须返回具有完全相同接口的对象,外加一个称为module的附加属性。 module属性用作模块的名称,并且应与模块的类名相同,如下例所示。
提示
对于动态模块,模块选项对象的所有属性都是可选的,模块除外。
静态 register()
方法呢? 现在我们可以看到它的工作是返回具有 DynamicModule
接口的对象。 当我们调用它时,我们实际上是在导入列表中提供一个模块,类似于在静态情况下通过列出模块类名的方式。 换句话说,动态模块 API 只是返回一个模块,而不是固定 @Modules
装饰器中的属性,而是通过编程方式指定它们。
仍然有一些细节需要详细了解:
现在我们可以声明
@Module()
装饰器的imports
属性不仅可以使用一个模块类名(例如,imports: [UsersModule]) ,还可以使用一个返回动态模块的函数(例如,imports: [ConfigModule.register(...)])。动态模块本身可以导入其他模块。 如果动态模块依赖于其他模块的提供程序,则可以使用可选的 imports 属性导入它们。 同样,这与使用 @Module() 装饰器为静态模块声明元数据的方式完全相似。
动态 ConfigModule 声明
import { DynamicModule, Module } from '@nestjs/common';
import { ConfigService } from './config.service';
@Module({})
export class ConfigModule {
static register(): DynamicModule {
return {
module: ConfigModule,
providers: [ConfigService],
exports: [ConfigService],
};
}
}
调用 ConfigModule.register(...)
将返回一个 DynamicModule 对象,该对象的属性基本上与我们通过 @Module()
装饰器提供的元数据相同。
# 模块配置
定制 ConfigModule
行为的解决方案是在静态 register()
方法中向其传递一个 options 对象。
import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { ConfigModule } from './config/config.module';
@Module({
imports: [ConfigModule.register({ folder: './config' })],
controllers: [AppController],
providers: [AppService],
})
export class AppModule {}
对服务进行一些更改,以便基于 options 对象的属性自定义其行为。 硬编码示例:
import { Injectable } from '@nestjs/common';
import * as dotenv from 'dotenv';
import * as fs from 'fs';
import { EnvConfig } from './interfaces';
@Injectable()
export class ConfigService {
private readonly envConfig: EnvConfig;
constructor() {
const options = { folder: './config' };
const filePath = `${process.env.NODE_ENV || 'development'}.env`;
const envFile = path.resolve(__dirname, '../../', options.folder, filePath);
this.envConfig = dotenv.parse(fs.readFileSync(envFile));
}
get(key: string): string {
return this.envConfig[key];
}
}
ConfigModule
提供 ConfigService
。而 ConfigService
又依赖于只在运行时提供的 options
对象。因此,在运行时,我们需要首先将 options
对象绑定到 Nest IoC
容器,然后让 Nest
将其注入 ConfigService
。
将 options
对象绑定到 IoC
容器的问题。我们在静态 register()
方法中执行此操作。请记住,我们正在动态地构造一个模块,而模块的一个属性就是它的提供者列表。因此,我们需要做的是将 options 对象定义为提供程序。在下面的代码中,注意 provider 数组:
import { DynamicModule, Module } from '@nestjs/common';
import { ConfigService } from './config.service';
@Module({})
export class ConfigModule {
static register(options): DynamicModule {
return {
module: ConfigModule,
providers: [
{
provide: 'CONFIG_OPTIONS',
useValue: options,
},
ConfigService,
],
exports: [ConfigService],
};
}
}
现在,我们可以通过将 CONFIG_OPTIONS
提供者注入 ConfigService
来完成这个过程。
import { Injectable, Inject } from '@nestjs/common';
import * as dotenv from 'dotenv';
import * as fs from 'fs';
import { EnvConfig } from './interfaces';
@Injectable()
export class ConfigService {
private readonly envConfig: EnvConfig;
constructor(@Inject('CONFIG_OPTIONS') private options) {
const filePath = `${process.env.NODE_ENV || 'development'}.env`;
const envFile = path.resolve(__dirname, '../../', options.folder, filePath);
this.envConfig = dotenv.parse(fs.readFileSync(envFile));
}
get(key: string): string {
return this.envConfig[key];
}
}
export const CONFIG_OPTIONS = 'CONFIG_OPTIONS';
# 04. 注入作用域
# 提供者范围
基本上,每个提供者都可以作为一个单例,被请求范围限定,并切换到瞬态模式。请参见下表,以熟悉它们之间的区别。
- DEFAULT:每个提供者可以跨多个类共享。提供者生命周期严格绑定到应用程序生命周期。一旦应用程序启动,所有提供程序都已实例化。默认情况下使用单例范围。
- REQUEST:在请求处理完成后,将为每个传入请求和垃圾收集专门创建提供者的新实例
- TRANSIENT:临时提供者不能在提供者之间共享。每当其他提供者向 Nest 容器请求特定的临时提供者时,该容器将创建一个新的专用实例
提示
对于大多数用例,建议使用单例范围。请求之间共享提供者可以降低内存消耗,从而提高应用程序的性能(不需要每次实例化类)。
# 用法
向 @Injectable() 装饰器传递一个选项对象。
import { Injectable, Scope } from '@nestjs/common';
@Injectable({ scope: Scope.REQUEST })
export class CatsService {}
在自定义提供者的情况下,必须设置一个额外的范围属性。
{
provide: 'CACHE_MANAGER',
useClass: CacheManager,
scope: Scope.TRANSIENT,
}
提示
网关不应该使用请求范围提供者,因为其必须作为单例提供。每个网关都封装了一个socket并且不能多次实例化。
默认使用单例范围,并且不需要声明。如果你想声明一个单例范围的提供者,在scope属性中使用Scope.DEFAULT值。
# 控制器范围
控制器也可以有范围,它适用于在该控制器中声明的所有请求方法处理程序。与提供者作用域一样,控制器的作用域声明了它的生命周期。对于请求范围的控制器,为每个入站请求创建一个新实例,并在请求完成处理时进行垃圾收集。
scope
使用对象的属性声明控制器范围ControllerOptions:
@Controller({
path: 'cats',
scope: Scope.REQUEST,
})
export class CatsController {}
提示
网关永远不应该依赖于请求范围的提供者,因为它们充当单例。一个网关封装了一个真正的套接字,不能多次被实例化
# 作用域分级
REQUEST
范围在注入链上冒泡。依赖于请求作用域的控制器,其本身也是请求作用域。
想象一下下面的链: CatsController <- CatsService <- CatsRepository
.如果CatsService
是请求域的(而其他的是默认的单例),那么CatsController
将成为请求域的,因为它依赖于注入的服务。而不依赖的CatsRepository将保持单例。
瞬时作用的依赖关系并不遵循这种模式。如果一个单例的DogsService注入了一个瞬时的LoggerService提供者,它将收到一个新的实例。然而,DogsService将保持单例,所以在任何地方注入它都不会解析为DogsService的一个新实例。如果这是所希望的行为,DogsService必须明确地被标记为TRANSIENT
。
# 请求提供者
在 HTTP
应用程序中(例如使用@nestjs/platform-express
或@nestjs/platform-fastify
),当使用请求范围提供者时,可能需要获取原始的请求对象。可以通过注入REQUEST
对象来做到这一点。
import { Injectable, Scope, Inject } from '@nestjs/common';
import { REQUEST } from '@nestjs/core';
import { Request } from 'express';
@Injectable({ scope: Scope.REQUEST })
export class CatsService {
constructor(@Inject(REQUEST) private request: Request) {}
}
# 性能
使用请求范围的提供者将明显影响应用程序性能。即使 Nest 试图缓存尽可能多的元数据,它仍然必须为每个请求创建类的实例。因此,它将降低平均响应时间和总体基准测试结果。如果提供者不一定需要请求范围,应该坚持使用单例范围。
# 05. 循环依赖
当两个类互相依赖时就会出现循环依赖. 例如,当 A 类需要 B 类,而 B 类也需要 A 类时,就会产生循环依赖。Nest 允许在提供者( provider )和模块( module )之间创建循环依赖关系.
建议尽可能避免循环依赖。但是有时候难以避免,Nest
提供了两个方法来解决这个问题。即正向引用(forward reference)
和模块引用(ModuleRef)
来从注入容器中获取一个提供者。
# 正向引用
正向引用允许 Nest
引用目前尚未被定义的引用。当CatsService
和 CommonService
相互依赖时,关系的双方都需要使用 @Inject()
和 forwardRef()
,否则 Nest
不会实例化它们,因为所有基本元数据都不可用。让我们看看下面的代码片段
// cats.service.ts
@Injectable()
export class CatsService {
constructor(
@Inject(forwardRef(() => CommonService))
private readonly commonService: CommonService,
) {}
}
这只是关系的一方面。现在让我们对 CommonService 做同样的事情
// common.service.ts
@Injectable()
export class CommonService {
constructor(
@Inject(forwardRef(() => CatsService))
private catsService: CatsService,
) {}
}
提示
实例化的顺序是不确定的。确保代码不依赖于首先调用哪个构造函数。具有循环依赖关系依赖于具有Scope.REQUEST
的提供程序可能会导致未定义的依赖关系
# 可选的模块引用(ModuleRef)类
一个选择是使用forwardRef()来重构你的代码,并使用ModuleRef类来在循环引用关系一侧获取提供者。更多关于ModuleRef类的内容参考这里。
# 模块正向引用
为了处理模块( module )之间的循环依赖,必须在模块关联的两个部分上使用相同的 forwardRef():
// common.module.ts
@Module({
imports: [forwardRef(() => CatsModule)],
})
export class CommonModule {}
# 06. 模块参考
Nest
提供了一个ModuleRef
类来导航到内部提供者列表,并使用注入令牌作为查找键名来获取一个引用。ModuleRef
类也提供了一个动态实例化静态和范围的提供者的方法。ModuleRef
可以通过常规方法注入到类中:
// cats.service.tsJS
import { ModuleRef } from '@nestjs/core'
@Injectable()
export class CatsService {
constructor(private moduleRef: ModuleRef) {}
}
# 获取实例
ModuleRef
实例拥有get()
方法。该方法获取一个提供者,控制器或者通过注入令牌/类名获取一个在当前模块中可注入对象(例如守卫或拦截器等)。
//cats.service.ts
import { ModuleRef } from '@nestjs/core'
@Injectable()
export class CatsService implements OnModuleInit {
private service: Service;
constructor(private moduleRef: ModuleRef) {}
onModuleInit() {
this.service = this.moduleRef.get(Service);
}
}
提示
不能通过get()方法获取一个范围的提供者(暂态的或者请求范围的)。
要从全局上下文获取一个提供者(例如,如果提供者在不同模块中注入),向get()的第二个参数传递{ strict: false }选项。
this.moduleRef.get(Service, { strict: false });
# 处理范围提供者
要动态处理一个范围提供者(瞬态的或请求范围的),使用resolve()
方法并将提供者的注入令牌作为参数提供给方法。
// cats.service.ts
import { ModuleRef } from '@nestjs/core'
@Injectable()
export class CatsService implements OnModuleInit {
private transientService: TransientService;
constructor(private moduleRef: ModuleRef) {}
async onModuleInit() {
this.transientService = await this.moduleRef.resolve(TransientService);
}
}
resolve()
方法从其自身的注入容器树返回一个提供者的唯一实例。每个子树都有一个独一无二的上下文引用。因此如果你调用该方法一次以上并进行引用比较的话,结果是不同的。
import { ModuleRef } from '@nestjs/core'
@Injectable()
export class CatsService implements OnModuleInit {
constructor(private moduleRef: ModuleRef) {}
async onModuleInit() {
const transientServices = await Promise.all([
this.moduleRef.resolve(TransientService),
this.moduleRef.resolve(TransientService),
]);
console.log(transientServices[0] === transientServices[1]); // false
}
}
要在不同的resolve()
调用之间产生一个单例,并保证他们共享同样生成的DI
容器子树,向resolve()
方法传递一个上下文引用,使用ContextIdFactory
类来生成上下文引用。该类提供了一个create()
方法,返回一个合适的独一无二的引用。
import {ContextIdFactory, ModuleRef} from '@nestjs/core'
@Injectable()
export class CatsService implements OnModuleInit {
constructor(private moduleRef: ModuleRef) {}
async onModuleInit() {
const contextId = ContextIdFactory.create();
const transientServices = await Promise.all([
this.moduleRef.resolve(TransientService, contextId),
this.moduleRef.resolve(TransientService, contextId),
]);
console.log(transientServices[0] === transientServices[1]); // true
}
}
# 注册REQUEST提供者
手动生成的上下文标识符(用ContextIdFactory.create())代表DI子树,其中REQUEST提供者是未定义的,因为它们没有被Nest依赖注入系统实例化和管理。
要为手动创建的DI
子树注册一个自定义REQUEST
对象,使用如下所示方法。
const contextId = ContextIdFactory.create();
this.moduleRef.registerRequestByContextId(/* YOUR_REQUEST_OBJECT */, contextId);
# 获取当前子树
有时,也需要在请求上下文中获取一个请求范围提供者的实例。例如,CatsService
是请求范围的,要获取的CatsRepository
实例也被标识为请求范围。要分享同一个注入容器子树,你需要获取当前上下文引用而不是生成一个新的(像前面的ContextIdFactory.create()
函数)。使用@Inject()
来获取当前的请求对象。
// cats.service.ts
@Injectable()
export class CatsService {
constructor(
@Inject(REQUEST) private request: Record<string, unknown>,
) {}
}
使用ContextIdFactory
类的getByRequest()
方法来基于请求对象创建一个上下文id 并传递resolve()
调用:
const contextId = ContextIdFactory.getByRequest(this.request);
const catsRepository = await this.moduleRef.resolve(CatsRepository, contextId);
# 动态实例化自定义类
要动态实例化一个之前未注册的类作为提供者,使用模块引用的create()方法。
// cats.service.ts
@Injectable()
export class CatsService implements OnModuleInit {
private catsFactory: CatsFactory;
constructor(private moduleRef: ModuleRef) {}
async onModuleInit() {
this.catsFactory = await this.moduleRef.create(CatsFactory);
}
}
# 07. 懒加载模块
默认情况下,模块是急切加载的,这意味着一旦应用程序加载,所有模块也会加载,无论它们是否立即需要。虽然这对大多数应用程序来说都很好,但它可能成为在无服务器环境中运行的应用程序/工作人员的瓶颈,其中启动延迟(“冷启动”)至关重要。
延迟加载可以通过仅加载特定无服务器函数调用所需的模块来帮助减少引导时间。此外,您还可以在无服务器功能“热启”后异步加载其他模块,以进一步加快后续调用的引导时间(延迟模块注册)。
为了按需加载模块,Nest 提供了LazyModuleLoader
可以以正常方式注入到类中的类:
import { LazyModuleLoader } from '@nestjs/core';
// cats.service.ts
@Injectable()
export class CatsService {
constructor(private lazyModuleLoader: LazyModuleLoader) {}
}
或者,可以从应用程序引导文件 (main.ts )中获取对提供程序的懒加载LazyModuleLoader
引用,如下所示:
// "app" represents a Nest application instance
const lazyModuleLoader = app.get(LazyModuleLoader);
有了上述,现在可以使用以下结构加载任何模块:
const { LazyModule } = await import('./lazy.module');
const moduleRef = await this.lazyModuleLoader.load(() => LazyModule);
提示
延迟加载
模块在第一次方法调用时被缓存。LazyModuleLoader#load
这意味着,每次连续的加载尝试LazyModule
都会非常快,并且会返回一个缓存的实例,而不是再次加载模块。
Load "LazyModule" attempt: 1
time: 2.379ms
Load "LazyModule" attempt: 2
time: 0.294ms
Load "LazyModule" attempt: 3
time: 0.303ms
此外,延迟加载
模块与那些在应用程序引导程序上急切加载的模块以及在应用程序中注册的任何其他延迟模块共享相同的模块图。
当lazy.module.ts
是是一个TypeScript
文件 那可以导出为常规 Nest 模块(无需额外更改)。
该LazyModuleLoader#load
方法返回模块引用(of LazyModule
),它允许您导航提供者的内部列表并使用其注入令牌作为查找键获取对任何提供者的引用。
例如,假设我们有LazyModule以下定义:
@Module({
providers: [LazyService],
exports: [LazyService],
})
export class LazyModule {}
提示
延迟加载的模块不能注册为全局模块,因为它根本没有意义(因为它们是延迟注册的,当所有静态注册的模块都已经实例化时按需注册)。同样,注册的全局增强器(守卫/拦截器/等)也无法正常工作。
有了这个,我们可以获得对LazyService
提供者的引用,如下所示:
const { LazyModule } = await import('./lazy.module');
const moduleRef = await this.lazyModuleLoader.load(() => LazyModule);
const { LazyService } = await import('./lazy.service');
const lazyService = moduleRef.get(LazyService);
提示
如果使用Webpack,请确保更新tsconfig.json文件 - 设置compilerOptions.module并"esnext"添加compilerOptions.moduleResolution属性"node"作为值:
{ "compilerOptions": { "module": "esnext", "moduleResolution": "node", } }
# 延迟加载控制器、网关和解析器
由于 Nest
中的控制器(或 GraphQL
应用程序中的解析器)表示一组路由/路径/主题(或查询/突变),因此不能使用LazyModuleLoader
该类延迟加载它们。
# 08. 应用上下文
Nest提供了一些应用类来简化在不同应用上下文之间编写应用(例如Nest HTTP应用,微服务和WebSockets应用)。这些应用可以用于创建通用的守卫,过滤器和拦截器,可以工作在控制器,方法和应用上下文中。
# ArgumentsHost类
ArgumentsHost
类提供了获取传递给处理程序的参数。它允许选择合适的上下文(例如HTTP,RPC(微服务)或者Websockets)来从框架中获取参数。框架提供了ArgumentsHost
的实例,作为host
参数提供给需要获取的地方。例如,在异常过滤器中传入ArgumentsHost
参数来调用catch()
方法。
ArgumentsHost
简单地抽象为处理程序参数。例如,在HTTP应用中(使用@nestjs/platform-express时),host对象封装了Express的[request, response, next]数组,reuest是一个request对象,response是一个response对象,next是控制应用的请求响应循环的函数。此外,在GraphQL应用中,host包含[root, args, context, info]数组。
# 当前应用上下文
当构建通用的守卫,过滤器和拦截器时,意味着要跨应用上下文运行,我们需要一种方法来确定我们的方法当前正在运行的应用程序类型。可以使用ArgumentsHost
的getType()
方法。
import { GqlContextType } from '@nestjs/graphql';
if (host.getType() === 'http') {
// do something that is only important in the context of regular HTTP requests (REST)
} else if (host.getType() === 'rpc') {
// do something that is only important in the context of Microservice requests
} else if (host.getType<GqlContextType>() === 'graphql') {
// do something that is only important in the context of GraphQL requests
}
# Host处理程序参数
要获取传递给处理程序的参数数组,使用host
对象的getArgs()
方法。
const [req, res, next] = host.getArgs();
可以使用getArgByIndex()
根据索引获取指定参数:
const request = host.getArgByIndex(0);
const response = host.getArgByIndex(1);
在这些例子中我们通过索引来获取请求响应对象,这并不推荐,因为它将应用和特定上下文耦合。为了使代码鲁棒性更好,更可复用,可以在程序中使用host对象的应用方法来切换合适的应用上下文,如下所示:
/**
* Switch context to RPC.
*/
switchToRpc(): RpcArgumentsHost;
/**
* Switch context to HTTP.
*/
switchToHttp(): HttpArgumentsHost;
/**
* Switch context to WebSockets.
*/
switchToWs(): WsArgumentsHost;
使用 switchToHttp()
方法重写前面的例子,host.switchToHttp()
帮助方法调用一个HTTP
应用的HttpArgumentsHost
对象. HttpArgumentsHost
对象有两个有用的方法,我们可以用来提取期望的对象。我们也可以使用Express类型的断言来返回原生的Express类型对象:
const ctx = host.switchToHttp();
const request = ctx.getRequest<Request>();
const response = ctx.getResponse<Response>();
类似地,WsArgumentsHost和RpcArgumentsHost有返回微服务和WebSockets上下文的方法,以下是WsArgumentsHost的方法:
export interface WsArgumentsHost {
/**
* Returns the data object.
*/
getData<T>(): T;
/**
* Returns the client object.
*/
getClient<T>(): T;
}
RpcArgumentsHost方法:
export interface RpcArgumentsHost {
/**
* Returns the data object.
*/
getData<T>(): T;
/**
* Returns the context object.
*/
getContext<T>(): T;
}
# 执行上下文类
ExecutionContext
扩展了ArgumentsHost
,提供有关当前执行过程的其他详细信息。和ArgumentsHost
类似,Nest
在需要的时候提供了一个ExecutionContext
的实例, 例如守卫的canActivate()
方法和拦截器的intercept()
方法,它提供以下方法:
export interface ExecutionContext extends ArgumentsHost {
/**
* Returns the type of the controller class which the current handler belongs to.
*/
getClass<T>(): Type<T>;
/**
* Returns a reference to the handler (method) that will be invoked next in the
* request pipeline.
*/
getHandler(): Function;
}
getHandler()
方法返回要调用的处理程序的引用。
getClass()
方法返回一个特定处理程序所属的控制器类。例如,一个HTTP
上下文,如果当前处理的是一个POST
请求,在CatsController
中绑定create()
方法。getHandler()
返回create()
方法和getClass()
方法所在的CatsController
类的引用(不是实例)。
const methodKey = ctx.getHandler().name; // "create"
const className = ctx.getClass().name; // "CatsController"
访问对当前类和处理程序方法的引用的能力提供了极大的灵活性。最重要的是,它让我们有机会通过@SetMetadata()
装饰器从守卫或拦截器中访问元数据集。我们将在下面介绍这个用例。
# 反射和元数据
Nest
提供了通过@SetMetadata()
装饰器将自定义元数据附加在路径处理程序的能力。我们可以在类中获取这些元数据来执行特定决策。
import {SetMetadata} from '@nestjs/common';
// cats.controller.ts
@Post()
@SetMetadata('roles', ['admin'])
async create(@Body() createCatDto: CreateCatDto) {
this.catsService.create(createCatDto);
}
基于上述结构,我们将roles元数据(roles是一个元数据,并且['admin'] 是对应的值)关联到create()方法。在这种情况下,不推荐直接在路径中使用@SetMetadata(),而是应该如下创建自己的装饰器:
//roles.decorator.ts
import { SetMetadata } from '@nestjs/common';
export const Roles = (...roles: string[]) => SetMetadata('roles', roles);
这种方法更干净、更易读,并且是强类型的。 现在可以使用自定义的@Roles()装饰器,并将其应用在create()方法中。
// cats.controller.ts
@Post()
@Roles('admin')
async create(@Body() createCatDto: CreateCatDto) {
this.catsService.create(createCatDto);
}
要访问roles路径 (自定义元数据),我们将使用Reflector
辅助类,它由框架提供,开箱即用,从@nestjs/core包导入。Reflector可以通过常规方式注入到类:
import { Reflector } from '@nestjs/core';
// roles.guard.ts
@Injectable()
export class RolesGuard {
constructor(private reflector: Reflector) {}
}
使用get()
方法读取处理程序的元数据。
const roles = this.reflector.get<string[]>('roles', context.getHandler());
Reflector#get
方法允许通过传递两个参数简单获取元数据:一个元数据key和一个context(装饰器对象)来获取元数据。
在本例中,指定的key是roles。context 由context.getHandler()提供,用于从当前路径处理程序中获取元数据,getHandler()给了我们一个到路径处理函数的引用。
我们也可以组织我们的控制器,来从控制器层获取元数据,以在控制器所有路径中应用。
// cats.controller.ts
@Roles('admin')
@Controller('cats')
export class CatsController {}
在本例中,要获取控制器元数据,将context.getClass()
作为第二个参数(将控制器类作为上下文提供以获取元数据)来替代context.getHandler()
:
// roles.guard.ts
const roles = this.reflector.get<string[]>('roles', context.getClass());
要具备在多层提供元数据的能力,需要从多个上下文获取与合并元数据。Reflector类提供两个应用方法来帮助实现该功能。这些方法同时获取控制器和方法元数据,并通过不同方法来合并他们。
考虑以下场景,在两个水平应用roles都提供了元数据:
// cats.controller.ts
@Roles('user')
@Controller('cats')
export class CatsController {
@Post()
@Roles('admin')
async create(@Body() createCatDto: CreateCatDto) {
this.catsService.create(createCatDto);
}
}
如果你想将user指定为默认角色,并且出于特定目的有选择地进行覆盖,可以使用getAllAndOverride()
方法。
const roles = this.reflector.getAllAndOverride<string[]>('roles', [
context.getHandler(),
context.getClass(),
]);
使用该代码编写守卫,在上下文中应用create()方法,采用上述元数据,将生成包含 ['admin']的roles。
要获取与合并元数据(该方法合并数组和对象),使用getAllAndMerge()方法:
const roles = this.reflector.getAllAndMerge<string[]>('roles', [
context.getHandler(),
context.getClass(),
]);
这会生成包含['user', 'admin']
的roles
。
对于这两种合并方法,传输元数据作为第一个参数,数组或者元数据对象上下文(例如,调用getHandler()和/或getClass())作为第二个参数。
# 09. 声明周期事件
所有应用程序元素都有一个由 Nest 管理的生命周期。Nest 提供了生命周期钩子,提供了对关键生命时刻的可见性,以及在关键时刻发生时采取行动(在你的module
,injectable
或者controller
中注册代码)的能力。
# 生命周期序列
下图描述了关键应用生命周期事件序列,从应用引导之时到node应用退出。我们可以把整个生命周期划分为三个阶段:初始化,运行和终止。使用生命周期,你可以合理计划模块和服务的初始化,管理活动链接,并且在应用程序收到终止指令时优雅地退出。
# 生命周期事件
生命周期事件在应用初始化
与终止
时发生。Nest在modules
,injectables
和controllers
的以下每个生命周期事件(首先要使能shutdown钩子,如下描述)中调用注册钩子方法。和上图所示的一样,Nest也调用合适的底层方法来监听连接,以及终止监听连接。
在下述表格中,onModuleDestroy, beforeApplicationShutdown和 onApplicationShutdown仅仅在显式调用app.close()或者应用收到特定系统信号(例如 SIGTERM)并且在初始化时(参见下表的应用shutdown部分)正确调用了enableShutdownHooks方法后被触发。
生命周期钩子方法 | 生命周期时间触发钩子方法调用 |
---|---|
OnModuleInit() | 初始化主模块依赖处理后调用一次 |
OnApplicationBootstrap() | 在应用程序完全启动并监听连接后调用一次 |
OnModuleDestroy() | 收到终止信号(例如SIGTERM)后调用 |
beforeApplicationShutdown() | 在onModuleDestroy()完成(Promise被resolved或者rejected);一旦完成,将关闭所有连接(调用app.close() 方法). |
OnApplicationShutdown() | 连接关闭处理时调用(app.close()) |
提示
上述列出的生命周期钩子没有被请求范围类触发。请求范围类并没有和生命周期以及不可预测的寿命绑定。他们为每个请求单独创建,并在响应发送后通过垃圾清理系统自动清理。
# 使用
所有应用周期的钩子都有接口表示,接口在技术上是可选的,因为它们在 TypeScript 编译之后就不存在了。尽管如此,为了从强类型和编辑器工具中获益,使用它们是一个很好的实践。要注册生命周期挂钩,请实现适当的接口。例如,要注册一个方法在特定类(例如,控制器,提供者或者模块)初始化时调用,使用OnModuleInit接口,提供onModuleInit()方法,如下:
import { Injectable, OnModuleInit } from '@nestjs/common';
@Injectable()
export class UsersService implements OnModuleInit {
onModuleInit() {
console.log(`The module has been initialized.`);
}
}
- 异步初始化
此外,OnModuleInit
和 OnApplicationBootstrap
钩子都允许延迟应用程序初始化过程(返回一个Promise或在方法主体中将方法标记为async和await异步方法)。
async onModuleInit(): Promise<void> {
await this.fetch();
}
# 应用程序关闭
onModuleDestroy()
, beforeApplicationShutdown()
和 onApplicationShutdown()
钩子程序响应系统终止信号(当应用程序通过显示调用app.close()或者收到SIGTERM系统信号时),以优雅地关闭 Nest 应用程序。这一功能通常用于 Kubernetes 、Heroku 或类似的服务。
系统关闭钩子消耗系统资源,因此默认是禁用的。要使用此钩子,必须通过enableShutdownHooks()
激活侦听器。
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
// Starts listening to shutdown hooks
app.enableShutdownHooks();
await app.listen(3000);
}
bootstrap();
提示
enableShutdownHooks
开始监听时消耗内存。如果要在一个单独Node线程中运行多个Nest应用(例如,使用多个Jest运行测试),Node会抱怨监听者太多。出于这个原因,enableShutdownHooks默认未启用。要在单个Node进程中运行多个实例时尤其要注意这一点。
如果应用程序接收到一个终止信号,它将会依次调用注册的onModuleDestroy()
,beforeApplicationShutdown()
和onApplicationShutdown()
方法,将响应信号作为第一个参数。如果一个注册函数等待异步调用(作为promise),那么在 promise 被解析或拒绝之前,它不会关闭 Nest 应用程序。
@Injectable()
class UsersService implements OnApplicationShutdown {
onApplicationShutdown(signal: string) {
console.log(signal); // e.g. "SIGINT"
}
}
提示
调用app.close()不会终止Node进程,只会触发onModuleDestroy()和onApplicationShutdown()钩子,所以如果有一些间隔,长时间运行的后台任务等,该进程不会自动终止。
# 10. 测试
自动化测试被认为是任何成熟的软件开发工作的一个重要组成部分。自动化使得在开发过程中快速、轻松地重复独立测试或者单元测试变得容易。这有助于确保版本满足质量和性能目标。自动化有助于提高覆盖率,并为开发人员提供一个更快的反馈回路。自动化既能提高单个开发人员的生产力,又能确保测试在关键的开发生命周期节点上运行,如源代码控制签入、功能集成和版本发布。
这种测试通常跨越多种类型,包括单元测试、端到端(e2e)测试、集成测试等等。虽然好处是毋庸置疑的,但设置它们可能会很繁琐。Nest努力促进开发的最佳实践,包括有效的测试,所以它包括以下功能,以帮助开发人员和团队建立和自动化测试。
- 自动为组件提供默认单元测试,为应用程序提供e2e测试。
- 提供默认的工具(如测试运行器,建立一个孤立的模块/应用加载器)。
- 提供
Jest
和Supertest
开箱即用的集成,兼容其他测试工具。 - 在测试环境中保证
Nest
依赖性注入系统,以便轻松模拟组件。
如前所述,你可以使用任何你喜欢的测试框架,因为Nest并不强制要求任何特定的工具。只需替换需要的元素(如测试运行器),你仍然可以享受Nest现成的测试设施的好处。
# 安装
开始工作,首先要安装所需的软件包。
npm i --save-dev @nestjs/testing
# 单元测试:
在下面的例子中,我们测试了两个类。CatsController
和CatsService
。如前所述,Jest被提供为默认的测试框架。它作为一个测试运行器,还提供了断言函数和测试双重工具。在下面的基本测试中,我们手动实例化这些类,并保证控制器和服务满足他们的API接口。
// cats.controller.spec.tsJS
import { CatsController } from './cats.controller';
import { CatsService } from './cats.service';
describe('CatsController', () => {
let catsController: CatsController;
let catsService: CatsService;
beforeEach(() => {
catsService = new CatsService();
catsController = new CatsController(catsService);
});
describe('findAll', () => {
it('should return an array of cats', async () => {
const result = ['test'];
jest.spyOn(catsService, 'findAll').mockImplementation(() => result);
expect(await catsController.findAll()).toBe(result);
});
});
});
# 测试工具
@nestjs/testing包提供了一组实用工具,使测试过程更加健全。让我们使用内置的测试类重写前面的例子。
// cats.controller.spec.ts
import { Test } from '@nestjs/testing';
import { CatsController } from './cats.controller';
import { CatsService } from './cats.service';
describe('CatsController', () => {
let catsController: CatsController;
let catsService: CatsService;
beforeEach(async () => {
const moduleRef = await Test.createTestingModule({
controllers: [CatsController],
providers: [CatsService],
}).compile();
catsService = moduleRef.get<CatsService>(CatsService);
catsController = moduleRef.get<CatsController>(CatsController);
});
describe('findAll', () => {
it('should return an array of cats', async () => {
const result = ['test'];
jest.spyOn(catsService, 'findAll').mockImplementation(() => result);
expect(await catsController.findAll()).toBe(result);
});
});
});
Test
类提供应用上下文以模拟整个Nest运行时,但提供了使管理类实例变得容易的钩子,包括模拟
和覆盖
,这一点很有用。 Test 类有一个 createTestingModule()
方法,该方法将模块的元数据(与在 @Module() 装饰器中传递的对象相同的对象)作为参数。这个方法创建了一个 TestingModule
实例,该实例提供了一些方法,但是当涉及到单元测试时,这些方法中只有 compile()
是有用的。这个方法初始化一个模块和它的依赖(和传统应用中从main.ts文件使用NestFactory.create()方法类似),并返回一个准备用于测试的模块。
提示
compile()
方法是异步的,因此必须等待执行完成。一旦模块编译完成,可以使用get()
方法获取任何声明的静态实例(控制器和提供者)。
TestingModule
继承自module reference类,因此具备动态处理提供者的能力(暂态的或者请求范围的),可以使用resolve()
方法(get()
方法只能检索获取静态实例).
const moduleRef = await Test.createTestingModule({
controllers: [CatsController],
providers: [CatsService],
}).compile();
catsService = await moduleRef.resolve(CatsService);
resolve()
方法从其自身的注入容器子树返回一个提供者的单例,每个子树都有一个独有的上下文引用。因此,如果你调用这个方法多次,可以看到它们是不同的。
# 自动模拟
Nest
还允许定义一个模拟工厂以应用于所有缺少的依赖项。这对于在一个类中有大量依赖项并且模拟所有依赖项需要很长时间和大量设置的情况很有用。要使用此功能,createTestingModule()
需要将方法与useMocker()
方法链接起来,为依赖模拟传递一个工厂。这个工厂可以接受一个可选的令牌,它是一个实例令牌,任何对 Nest 提供者有效的令牌,并返回一个模拟实现。
下面是创建通用模拟程序使用jest-mock和特定模拟程序CatsService使用的示例jest.fn()。
const moduleMocker = new ModuleMocker(global);
describe('CatsController', () => {
let controller: CatsController;
beforeEach(async () => {
const moduleRef = await Test.createTestingModule({
controllers: [CatsController],
})
.useMocker((token) => {
if (token === CatsService) {
return { findAll: jest.fn().mockResolvedValue(results) };
}
if (typeof token === 'function') {
const mockMetadata = moduleMocker.getMetadata(token) as MockFunctionMetadata<any, any>;
const Mock = moduleMocker.generateFromMetadata(mockMetadata);
return new Mock();
}
})
.compile();
controller = moduleRef.get(CatsController);
});
})
# 端到端测试
与关注单个模块和类的单元测试不同,端到端(e2e)测试涵盖了类和模块在更高层次上的交互--和生产环境下终端用户类似。随着应用程序的增长,手动测试每个API端点的端到端行为变得困难。自动化的端到端测试帮助我们确保系统的整体行为是正确的,并符合项目要求。为了进行e2e测试,我们使用类似于刚才单元测试中的配置。此外,Nest使我们能够轻松地使用Supertest库来模拟HTTP请求。
// cats.e2e-spec.ts
import * as request from 'supertest';
import { Test } from '@nestjs/testing';
import { CatsModule } from '../../src/cats/cats.module';
import { CatsService } from '../../src/cats/cats.service';
import { INestApplication } from '@nestjs/common';
describe('Cats', () => {
let app: INestApplication;
let catsService = { findAll: () => ['test'] };
beforeAll(async () => {
const moduleRef = await Test.createTestingModule({
imports: [CatsModule],
})
.overrideProvider(CatsService)
.useValue(catsService)
.compile();
app = moduleRef.createNestApplication();
await app.init();
});
it(`/GET cats`, () => {
return request(app.getHttpServer())
.get('/cats')
.expect(200)
.expect({
data: catsService.findAll(),
});
});
afterAll(async () => {
await app.close();
});
});
如果使用Fasify作为HTTP服务器,在配置上有所不同,其有一些内置功能:
let app: NestFastifyApplication;
beforeAll(async () => {
app = moduleRef.createNestApplication<NestFastifyApplication>(
new FastifyAdapter(),
);
await app.init();
await app.getHttpAdapter().getInstance().ready();
})
it(`/GET cats`, () => {
return app
.inject({
method: 'GET',
url: '/cats'
}).then(result => {
expect(result.statusCode).toEqual(200)
expect(result.payload).toEqual(/* expectedPayload */)
});
})
在这个例子中,我们使用了之前描述的概念,在之前使用的compile()
外,我们使用createNestApplication()
方法来实例化一个Nest运行环境。我们在app变量中储存了一个app引用以便模拟HTTP请求。
使用Supertest
的request()
方法来模拟HTTP请求。我们希望这些HTTP请求访问运行的Nest应用,因此向request()传递一个Nest底层的HTTP监听者(可能由Express平台提供),以此构建请求(app.getHttpServer()),调用request()交给我们一个包装的HTTP服务器以连接Nest应用,它暴露了模拟真实HTTP请求的方法。例如,使用request(...).get('/cats')将初始化一个和真实的从网络来的get '/cats'相同的HTTP请求。
在这个例子中,我们也提供了一个可选的 CatsService
应用,它返回一个硬编码值供我们测试。使用overrideProvider()
来进行覆盖替换。类似地,Nest也提供了覆盖守卫,拦截器,过滤器和管道的方法:overrideGuard()
, overrideInterceptor()
, overrideFilter()
, overridePipe()
。
每个覆盖方法返回包括3个不同的在自定义提供者中描述的方法镜像:
- useClass: 提供一个类来覆盖对象(提供者,守卫等)。
- useValue: 提供一个实例来覆盖对象。
- useFactory: 提供一个方法来返回覆盖对象的实例。
每个覆盖方法都返回TestingModule
实例,可以通过链式写法与其他方法连接。可以在结尾使用compile()方法以使Nest实例化和初始化模块。
此外,有时你可能想提供一个自定义的日志,例如,当测试运行时(例如,在CI服务器上)。使用setLogger()方法并传递一个满足LoggerService接口的对象来指示TestModuleBuilder如何在测试期间记录(默认情况下,只有 "错误 "日志会被记录到控制台)。
该编译模块有几个有用的方法,如下表所述:
- createNestInstance() 基于给定模块创建一个Nest实例(返回INestApplication),请注意,必须使用init()方法手动初始化应用程序
- createNestMicroservice() 基于给定模块创建Nest微服务实例(返回INestMicroservice)
- get() 从module reference类继承,检索应用程序上下文中可用的控制器或提供程序(包括警卫,过滤器等)的实例
- resolve() 从module reference类继承,检索应用程序上下文中控制器或提供者动态创建的范围实例(包括警卫,过滤器等)的实例
- select() 浏览模块树,从所选模块中提取特定实例(与get()方法中严格模式{strict:true}一起使用)
# 覆盖全局注册的强化程序
如果有一个全局注册的守卫 (或者管道,拦截器或过滤器),可能需要更多的步骤来覆盖他们。 将原始的注册做如下修改:
providers: [
{
provide: APP_GUARD,
useClass: JwtAuthGuard,
},
],
这样通过APP_*把守卫注册成了"multi"-provider。要在这里替换 JwtAuthGuard,应该在槽中使用现有提供者。
将useClass修改为useExisting来引用注册提供者,而不是在令牌之后使用Nest实例化。
providers: [
{
provide: APP_GUARD,
useExisting: JwtAuthGuard,
// notice the use of 'useExisting' instead of 'useClass'
},
JwtAuthGuard,
],
现在JwtAuthGuard
在Nest可以作为一个常规的提供者,也可以在创建TestingModule
时被覆盖 :
const moduleRef = await Test.createTestingModule({
imports: [AppModule],
})
.overrideProvider(JwtAuthGuard)
.useClass(MockAuthGuard)
.compile();
这样测试就会在每个请求中使用MockAuthGuard
。
# 测试请求范围实例
请求范围提供者针对每个请求创建。其实例在请求处理完成后由垃圾回收机制销毁。这产生了一个问题,因为我们无法针对一个测试请求获取其注入依赖子树。
resolve()方法可以用来获取一个动态实例化的类。因此,我们可以传递一个独特的上下文引用来控制注入容器子树的声明周期。如何来在测试上下文中暴露它呢?
[策略]是生成一个上下文向前引用并且强迫Nest使用这个特殊ID来为所有输入请求创建子树。这样我们就可以获取为测试请求创建的实例。
将jest.spyOn()
应用于ContextIdFactory
来实现此目的:
const contextId = ContextIdFactory.create();
jest
.spyOn(ContextIdFactory, 'getByRequest')
.mockImplementation(() => contextId);
可以使用这个contextId来在任何子请求中获取一个生成的注入容器子树。
catsService = await moduleRef.resolve(CatsService, contextId);