从头开始学习nestjs-第六章-技术-校验/缓存/序列化/任务调度

308 阅读9分钟

校验

最好的做法是验证发送到 Web 应用的任何数据的正确性。为了自动验证传入的请求,Nest 提供了几个开箱即用的管道:

  • ValidationPipe
  • ParseIntPipe
  • ParseBoolPipe
  • ParseArrayPipe
  • ParseUUIDPipe

ValidationPipe 使用强大的 class-validator 包及其声明式验证装饰器。ValidationPipe 提供了一种方便的方法来为所有传入的客户端有效负载执行验证规则,其中在每个 module 的本地类/DTO 声明中使用简单注释声明特定规则

概述

这里主要是了解一些 ValidationPipe 的各种实际用例,并使用其一些高级定制功能

使用内置的 ValidationPipe

安装所需的依赖

$ npm i --save class-validator class-transformer

提示:ValidationPipe 是从 @nestjs/common 包中导出的

因为这个管道使用了 class-validatorclass-transformer 库,所以有很多选项可用。可以通过传递给管道的配置对象来配置这些设置。以下是内置选项:

export interface ValidationPipeOptions extends ValidatorOptions {
  transform?: boolean;
  disableErrorMessages?: boolean;
  exceptionFactory?: (errors: ValidationError[]) => any;
}

除此之外,所有 class-validator 选项(继承自 ValidatorOptions 接口)都可用:

选项类型描述
enableDebugMessagesboolean如果设置为 true,当出现问题时,验证器将向控制台打印额外的警告消息
skipUndefinedPropertiesboolean如果设置为 true,则验证器将跳过验证对象中未定义的所有属性的验证
skipNullPropertiesboolean如果设置为 true,则验证器将跳过验证对象中所有为 null 的属性的验证
skipMissingPropertiesboolean如果设置为 true,则验证器将跳过验证对象中所有为 null 或未定义的属性的验证
whitelistboolean如果设置为 true,验证器将删除已验证(返回)对象的任何不使用任何验证装饰器的属性
forbidNonWhitelistedboolean如果设置为 true,验证器将抛出异常,而不是剥离非白名单属性
forbidUnknownValuesboolean如果设置为 true,尝试验证未知对象会立即失败
disableErrorMessagesboolean如果设置为 true,验证错误将不会返回给客户端
errorHttpStatusCodenumber此设置允许指定在发生错误时将使用哪种异常类型。默认情况下它抛出 BadRequestException
exceptionFactoryFunction获取验证错误数组并返回要抛出的异常对象
groupsstring[]验证对象期间要使用的组
alwaysboolean设置装饰器的 always 选项的默认值。可以在装饰器选项中覆盖默认值
strictGroupsboolean如果 groups 未给出或为空,则忽略至少一组的装饰器
dismissDefaultMessagesboolean如果设置为 true,验证将不会使用默认消息。如果未明确设置,错误消息始终为 undefined
validationError.targetboolean指示目标是否应在 ValidationError 中暴露
validationError.valueboolean指示验证值是否应在 ValidationError 中公开
stopAtFirstErrorboolean当设置为 true 时,给定属性的验证将在遇到第一个错误后停止。默认为 false

注意:在 repository 中查找有关 class-validator 软件包的更多信息

自动验证

在程序启动初始化的地方绑定 ValidationPipe 的话,所有端口都能受到保护,不会接收到不正确的数据

async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  app.useGlobalPipes(new ValidationPipe());
  await app.listen(3000);
}
bootstrap();

为了测试管道,创建一个基本端口:

@Post()
create(@Body() createUserDto: CreateUserDto) {
  return 'This action adds a new user';
}

提示:由于 TypeScript 不存储有关泛型或接口的元数据,因此在 DTO 中使用它们时,ValidationPipe 可能无法正确验证传入数据。为此,请考虑在 DTO 中使用具体类

导入 DTO 时,不能使用仅类型导入,因为这会在运行时被删除,即记住使用 import { CreateUserDto } 而不是 import type { CreateUserDto }

现在可以使用 class-validator 包提供的装饰器在 CreateUserDto 中添加一些验证规则,关于装饰器规则详细描述可以参考 此处。通过这种方式,任何使用 CreateUserDto 的路由都将自动执行这些验证规则

import { IsEmail, IsNotEmpty } from 'class-validator';

export class CreateUserDto {
  @IsEmail()
  email: string;

  @IsNotEmpty()
  password: string;
}

有了这些规则,如果在请求正文中使用无效的 email 属性,应用将自动响应 400 Bad Request 代码以及以下响应正文:

{
  "statusCode": 400,
  "error": "Bad Request",
  "message": ["email must be an email"]
}

除了验证请求主体之外,ValidationPipe 还可以与其他请求对象属性一起使用。想象一下,想在端口路径中接受:id。为确保此请求参数只接受数字,可以使用以下结构:

@Get(':id')
findOne(@Param() params: FindOneParams) {
  return 'This action returns a user';
}

FindOneParams 和 DTO 一样,只是一个使用 class-validator 定义验证规则的类。它看起来像这样:

import { IsNumberString } from 'class-validator';

export class FindOneParams {
  @IsNumberString()
  id: number;
}

禁用详细错误

错误消息有助于解释请求中的错误内容。但是,某些生产环境更喜欢禁用详细错误。通过将选项对象传递给 ValidationPipe 来执行此操作:

app.useGlobalPipes(
  new ValidationPipe({
    disableErrorMessages: true,
  }),
);

此时,详细的错误消息不会显示在响应正文中

剔除属性

ValidationPipe 还可以过滤掉不应由方法处理程序接收的属性。在这种情况下,可以将可接受的属性列入白名单,并且白名单中未包含的任何属性都会自动从生成的对象中删除。例如,如果处理程序需要 email 和 password 属性,但请求还包含 age 属性,则可以从生成的 DTO 中自动删除该属性。要启用此类行为,请将 whitelist 设置为 true

app.useGlobalPipes(
  new ValidationPipe({
    whitelist: true,
  }),
);

当设置为 true 时,这将自动删除非白名单属性(那些在验证类中没有任何装饰器的属性)

或者,可以在存在非白名单属性时停止处理请求,并向用户返回错误响应。要启用此功能,请将 forbidNonWhitelisted 选项属性设置为 true,同时将 whitelist 设置为 true

转换为具体实体类

通过网络传入的有效负载是纯 JavaScript 对象。ValidationPipe 可以自动将有效负载转换为根据其 DTO 类类型化的对象。要启用自动转换,请将 transform 设置为 true。这可以在方法级别完成:

@Post()
@UsePipes(new ValidationPipe({ transform: true }))
async create(@Body() createCatDto: CreateCatDto) {
  this.catsService.create(createCatDto);
}

要全局启用此行为,请在全局管道上设置选项:

app.useGlobalPipes(
  new ValidationPipe({
    transform: true,
  }),
);

启用自动转换选项后,ValidationPipe 还将执行基本类型的转换。在以下示例中,findOne() 方法采用一个参数,表示提取的 id 路径参数:

@Get(':id')
findOne(@Param('id') id: number) {
  console.log(typeof id === 'number'); // true
  return 'This action returns a user';
}

默认情况下,每个路径参数和查询参数都作为 string 通过网络传输。在上面的示例中,将 id 类型指定为 number(在方法签名中)。因此,ValidationPipe 会尝试自动将字符串标识符转换为数字

显式转换

可以使用 ParseIntPipeParseBoolPipe 显式转换值(请注意,不需要 ParseStringPipe,因为默认情况下每个路径参数和查询参数都作为 string 通过网络传输)

@Get(':id')
findOne(
  @Param('id', ParseIntPipe) id: number,
  @Query('sort', ParseBoolPipe) sort: boolean,
) {
  console.log(typeof id === 'number'); // true
  console.log(typeof sort === 'boolean'); // true
  return 'This action returns a user';
}

提示:ParseIntPipeParseBoolPipe 是从 @nestjs/common 包中导出的

映射类型(类的扩展)

当构建 CRUD(创建/读取/更新/删除)等功能时,在基本实体类型上构建变体通常很有用。Nest 提供了几个实用方法来执行类型转换,使这项任务更加方便

构建输入验证类型(也称为 DTO)时,在同一类型上构建创建和更新变体通常很有用。例如,创建变体可能需要所有字段,而更新变体可能使所有字段可选

Nest 提供了 PartialType() 实用方法来简化此任务并最大限度地减少样板文件

PartialType

PartialType() 方法返回一个类型(类),其中输入类型的所有属性都设置为可选。例如,假设有一个创建类型,如下所示:

export class CreateCatDto {
  name: string;
  age: number;
  breed: string;
}

默认情况下,所有这些字段都是必需的。要创建具有相同字段但每个字段都是可选的类型,请使用 PartialType() 并传递类引用 (CreateCatDto) :

export class UpdateCatDto extends PartialType(CreateCatDto) {}

提示:PartialType() 方法是从 @nestjs/mapped-types 包中导入的

PickType

PickType() 方法通过从输入类型中选取一组属性来构造一个新的类。假设定义一个创建类:

export class CreateCatDto {
  name: string;
  age: number;
  breed: string;
}

可以使用 PickType() 方法从此类中选择一组属性:

export class UpdateCatAgeDto extends PickType(CreateCatDto, ['age'] as const) {}

提示:PickType() 方法是从 @nestjs/mapped-types 包中导入的

OmitType

OmitType() 方法通过从输入类型中选取所有属性然后排除一组特定的键来构造一个新的类。假设定义一个创建类:

export class CreateCatDto {
  name: string;
  age: number;
  breed: string;
}

可以生成一个派生类型,它具有除 name 之外的所有属性,如下所示。在此构造中,OmitType 的第二个参数是一个属性名称数组

export class UpdateCatDto extends OmitType(CreateCatDto, ['name'] as const) {}

提示:OmitType() 方法是从 @nestjs/mapped-types 包中导入的

IntersectionType

IntersectionType() 方法将两种类合并为一种新的类。例如,假设从两种类型开始:

export class CreateCatDto {
  name: string;
  breed: string;
}

export class AdditionalCatInfo {
  color: string;
}

可以生成一个新类型,它结合了两种类型的所有属性:

export class UpdateCatDto extends IntersectionType(
  CreateCatDto,
  AdditionalCatInfo,
) {}

提示:IntersectionType() 方法是从 @nestjs/mapped-types 包中导入的

类型映射实用方法是可组合的。例如,以下将生成一个类型(类),该类型(类)具有 CreateCatDto 类型的所有属性(name 除外),并且这些属性将设置为可选:

export class UpdateCatDto extends PartialType(
  OmitType(CreateCatDto, ['name'] as const),
) {}

解析和验证数组

TypeScript 不存储有关泛型或接口的元数据,因此在 DTO 中使用它们时,ValidationPipe 可能无法正确验证传入数据。例如,在下面的代码中,createUserDtos 不会被正确验证:

@Post()
createBulk(@Body() createUserDtos: CreateUserDto[]) {
  return 'This action adds new users';
}

要验证数组,请创建一个包含封装数组的属性的专用类,或使用 ParseArrayPipe

@Post()
createBulk(
  @Body(new ParseArrayPipe({ items: CreateUserDto }))
  createUserDtos: CreateUserDto[],
) {
  return 'This action adds new users';
}

此外,ParseArrayPipe 在解析查询参数时可能会派上用场。考虑一个基于作为查询参数传递的标识符返回用户的 findByIds() 方法

@Get()
findByIds(
  @Query('ids', new ParseArrayPipe({ items: Number, separator: ',' }))
  ids: number[],
) {
  return 'This action returns users by ids';
}

此构造验证来自 HTTP GET 请求的传入查询参数,如下所示:

GET /?ids=1,2,3

WebSocket 和微服务

无论使用何种传输方法,ValidationPipe 都适用于 WebSocket 和微服务

缓存

缓存有助于提高应用的性能。它充当提供高性能数据访问的临时数据存储

安装

安装需要的包:

$ npm install @nestjs/cache-manager cache-manager

警告:cache-manager 版本 4 对 TTL (Time-To-Live) 使用秒。cache-manager (v5) 的当前版本已改为使用毫秒。NestJS 不转换值,只是将你提供的 ttl 转发给库。换句话说:

  • 如果使用 cache-manager v4,以秒为单位提供 ttl
  • 如果使用 cache-manager v5,以毫秒为单位提供 ttl
  • 文档指的是秒,因为 NestJS 是针对缓存管理器版本 4 发布的

内存缓存

Nest 为各种缓存存储提供器提供统一的 API。内置的是内存数据存储。可以轻松切换到更全面的解决方案,例如 Redis

为了启用缓存,导入 CacheModule 并调用其 register() 方法

import { Module } from '@nestjs/common';
import { CacheModule } from '@nestjs/cache-manager';
import { AppController } from './app.controller';

@Module({
  imports: [CacheModule.register()],
  controllers: [AppController],
})
export class AppModule {}

与缓存存储交互

要与缓存管理器实例交互,请使用 CACHE_MANAGER token 将其注入类中,如下所示:

constructor(@Inject(CACHE_MANAGER) private cacheManager: Cache) {}

提示:Cache 类是从 cache-manager 中导入的,而 CACHE_MANAGER token是从 @nestjs/cache-manager 包中导入的

Cache 实例(来自 cache-manager 包)上的 get 方法用于从缓存中查找内容。如果缓存中不存在该项,则返回 null

const value = await this.cacheManager.get('key');

要将内容添加到缓存,请使用 set方法:

await this.cacheManager.set('key', 'value');

缓存的默认过期时间为 5 秒

可以为此特定密钥手动指定 TTL(以秒为单位的过期时间),如下所示:

await this.cacheManager.set('key', 'value', 1000);

要禁用缓存过期,请将 ttl 配置属性设置为 0:

await this.cacheManager.set('key', 'value', 0);

要从缓存中删除内容,请使用 del 方法:

await this.cacheManager.del('key');

要清除整个缓存,请使用 reset 方法:

await this.cacheManager.reset();

自动缓存响应

警告:在 GraphQL 应用中,拦截器是为每个字段解析器单独执行的。因此,CacheModule(使用拦截器来缓存响应)将无法正常工作

要启用自动缓存响应,只需将 CacheInterceptor 绑定到要缓存数据的位置

@Controller()
@UseInterceptors(CacheInterceptor)
export class AppController {
  @Get()
  findAll(): string[] {
    return [];
  }
}

警告:仅缓存 GET 端口。此外,注入原生响应对象 (@Res()) 的 HTTP 服务器路由不能使用缓存拦截器

为了减少需要的模版文件的数量,可以将 CacheInterceptor 全局绑定到所有端口:

import { Module } from '@nestjs/common';
import { CacheModule, CacheInterceptor } from '@nestjs/cache-manager';
import { AppController } from './app.controller';
import { APP_INTERCEPTOR } from '@nestjs/core';

@Module({
  imports: [CacheModule.register()],
  controllers: [AppController],
  providers: [
    {
      provide: APP_INTERCEPTOR,
      useClass: CacheInterceptor,
    },
  ],
})
export class AppModule {}

自定义缓存

所有缓存数据都有自己的过期时间 (TTL) 。要自定义默认值,请将选项对象传递给 register() 方法

CacheModule.register({
  ttl: 5, // seconds
  max: 10, // maximum number of items in cache
});

全局使用module

想在其他 module 中使用 CacheModule 时,需要导入它。或者通过将选项对象的 isGlobal 属性设置为 true 来将其声明为全局 module ,如下所示。在这种情况下,一旦 CacheModule 被加载到根 module (例如,AppModule)中,就不需要在其他 module 中导入它

CacheModule.register({
  isGlobal: true,
});

全局缓存覆盖

启用全局缓存后,缓存条目存储在根据路由路径自动生成的 CacheKey 下。可以在每个方法的基础上覆盖某些缓存设置(@CacheKey()@CacheTTL()),从而允许为各个控制器方法自定义缓存策略

@Controller()
export class AppController {
  @CacheKey('custom_key')
  @CacheTTL(20)
  findAll(): string[] {
    return [];
  }
}

提示: @CacheKey()@CacheTTL() 装饰器是从 @nestjs/cache-manager 包导入的

@CacheKey()装饰器可以带@CacheTTL()装饰器,也可以不带,反之亦然。可以选择只覆盖@CacheKey()或者只覆盖@CacheTTL(),未被装饰器覆盖的设置将使用全局注册的默认值

WebSocket 和微服务

还可以将 CacheInterceptor 应用于 WebSocket 订阅者以及微服务的模式(无论使用何种传输方法)

@CacheKey('events')
@UseInterceptors(CacheInterceptor)
@SubscribeMessage('events')
handleEvent(client: Client, data: string[]): Observable<string[]> {
  return [];
}

但是,需要额外的 @CacheKey() 装饰器来指定用于随后存储和检索缓存数据的键。另外,请注意,不应该缓存所有内容。执行某些业务操作而不是简单地查询数据的操作不应该被缓存

此外,可以使用 @CacheTTL() 装饰器指定缓存过期时间 (TTL),这将覆盖全局默认 TTL 值

@CacheTTL(10)
@UseInterceptors(CacheInterceptor)
@SubscribeMessage('events')
handleEvent(client: Client, data: string[]): Observable<string[]> {
  return [];
}

提示: @CacheTTL() 装饰器可以与相应的 @CacheKey() 装饰器一起使用,也可以不与相应的 @CacheKey() 装饰器一起使用

调整跟踪

默认情况下,Nest 使用请求 URL(在 HTTP 应用中)或缓存键(在 websockets 和微服务应用中,通过 @CacheKey() 装饰器设置)将缓存记录与端口相关联。然而,有时可能希望根据不同因素设置跟踪,例如,使用 HTTP 标头(例如 Authorization 以正确识别 profile 端点)

为此,创建 CacheInterceptor 的子类并覆盖 trackBy() 方法

@Injectable()
class HttpCacheInterceptor extends CacheInterceptor {
  trackBy(context: ExecutionContext): string | undefined {
    return 'key';
  }
}

不同的存储

该服务在底层利用了 cache-manager。cache-manager 包支持作用域广泛的有用存储,例如 Redis 存储。此处 提供了受支持存储的完整列表。要设置 Redis 缓存,只需将包和相应的选项一起传递给 register() 方法

import type { RedisClientOptions } from 'redis';
import * as redisStore from 'cache-manager-redis-store';
import { Module } from '@nestjs/common';
import { CacheModule } from '@nestjs/cache-manager';
import { AppController } from './app.controller';

@Module({
  imports: [
    CacheModule.register<RedisClientOptions>({
      store: redisStore,

      // Store-specific configuration:
      host: 'localhost',
      port: 6379,
    }),
  ],
  controllers: [AppController],
})
export class AppModule {}

警告:cache-manager-redis-store 不支持 redis v4。为了使 ClientOpts 接口存在并正常工作,需要安装最新的 redis 3.x.x 主要版本

异步配置

可能希望异步传递 module 选项,而不是在编译时静态传递它们。在这种情况下,使用 registerAsync() 方法,它提供了几种处理异步配置的方法

一种方法是使用工厂函数:

CacheModule.registerAsync({
  useFactory: () => ({
    ttl: 5,
  }),
});

工厂的行为与所有其他异步 module 工厂一样(它可以是 async 并且能够通过 inject 注入依赖)

CacheModule.registerAsync({
  imports: [ConfigModule],
  useFactory: async (configService: ConfigService) => ({
    ttl: configService.get('CACHE_TTL'),
  }),
  inject: [ConfigService],
});

或者,可以使用 useClass 方法:

CacheModule.registerAsync({
  useClass: CacheConfigService,
});

上面的构造将在 CacheModule 中实例化 CacheConfigService,并将使用它来获取选项对象。CacheConfigService 必须实现 CacheOptionsFactory 接口才能提供配置选项:

@Injectable()
class CacheConfigService implements CacheOptionsFactory {
  createCacheOptions(): CacheModuleOptions {
    return {
      ttl: 5,
    };
  }
}

如果希望使用从不同 module 导入的现有配置 provider,请使用 useExisting 语法:

CacheModule.registerAsync({
  imports: [ConfigModule],
  useExisting: ConfigService,
});

这与 useClass 的工作原理相同,但有一个关键区别 - CacheModule 将查找导入的 module 以重用任何已创建的 ConfigService,而不是实例化自己的 module

提示:CacheModule#registerCacheModule#registerAsyncCacheOptionsFactory 有一个可选的泛型(类型参数)来缩小特定于存储的配置选项,使其类型安全

序列化

概述

Nest 提供了一个内置功能来帮助确保这些操作可以以一种直接的方式执行。ClassSerializerInterceptor 拦截器使用强大的 class-transformer 包来提供一种声明式和可扩展的对象转换方式。它执行的基本操作是获取方法处理程序返回的值并从 class-transformer 应用 instanceToPlain() 函数。这样做时,它可以在实体/DTO 类上应用 class-transformer 装饰器表达的规则

提示:序列化不适用于流式文件的响应

排除属性

假设想要从用户实体中自动排除 password 属性。将实体注释如下:

import { Exclude } from 'class-transformer';

export class UserEntity {
  id: number;
  firstName: string;
  lastName: string;

  @Exclude()
  password: string;

  constructor(partial: Partial<UserEntity>) {
    Object.assign(this, partial);
  }
}

现在考虑一个带有返回此类实例的方法处理程序的 controller

@UseInterceptors(ClassSerializerInterceptor)
@Get()
findOne(): UserEntity {
  return new UserEntity({
    id: 1,
    firstName: 'Kamil',
    lastName: 'Mysliwiec',
    password: 'password',
  });
}

警告:请注意,必须返回该类的一个实例。如果返回一个纯 JavaScript 对象,例如 { user: new UserEntity()},则该对象将不会被正确序列化

提示:ClassSerializerInterceptor 是从 @nestjs/common 导入的

请求此端口时,客户端会收到以下响应:

{
  "id": 1,
  "firstName": "Kamil",
  "lastName": "Mysliwiec"
}

请注意,拦截器可以应用于全局作用域。拦截器和实体类声明的组合确保任何返回 UserEntity 的方法都一定会删除 password 属性。这提供了集中执行此业务规则的措施

暴露属性

可以使用 @Expose() 装饰器为属性提供别名,或执行方法来计算属性值(类似于 getter 函数),如下所示

@Expose()
get fullName(): string {
  return `${this.firstName} ${this.lastName}`;
}

转换

可以使用 @Transform() 装饰器执行其他数据转换。例如,以下构造返回 RoleEntity 的名称属性,而不是返回整个对象

@Transform(({ value }) => value.name)
role: RoleEntity;

通过选项

可能想要修改转换方法的默认行为。要覆盖默认设置,请使用 @SerializeOptions() 装饰器将它们传递到 options 对象中

@SerializeOptions({
  excludePrefixes: ['_'],
})
@Get()
findOne(): UserEntity {
  return new UserEntity();
}

提示: @SerializeOptions() 装饰器是从 @nestjs/common 导入的

通过 @SerializeOptions() 传递的选项作为底层 instanceToPlain() 方法的第二个参数传递。在此示例中,自动排除以_前缀开头的所有属性

WebSocket 和微服务

无论使用何种传输方法,ClassSerializerInterceptor 都适用于 WebSockets 和微服务

任务调度(定时器)

任务调度允许安排任意代码(方法/函数)在固定日期/时间、重复间隔或在指定间隔后执行一次。在 Linux 世界中,这通常由操作系统级别的 cron 等软件包处理。对于 Node.js 应用,有几个包可以模拟类似 cron 的功能。Nest 提供了 @nestjs/schedule 包,它集成了流行的 Node.js cron

安装

安装依赖

$ npm install --save @nestjs/schedule

要激活工作调度,将 ScheduleModule 导入根 AppModule 并运行 forRoot() 静态方法,如下所示:

import { Module } from '@nestjs/common';
import { ScheduleModule } from '@nestjs/schedule';

@Module({
  imports: [
    ScheduleModule.forRoot()
  ],
})
export class AppModule {}

.forRoot() 调用初始化调度程序并注册应用中存在的任何声明性 定时任务timeoutsintervals。当 onApplicationBootstrap 生命周期钩子发生时注册发生,确保所有 module 都已加载并声明任何计划的工作

声明式 cron 工作

cron 工作安排任意函数(方法调用)自动运行。Cron 工作可以运行:

  • 一次,在指定的日期/时间
  • 在经常性的基础上;重复工作可以在指定时间间隔内的指定时刻运行(例如,每小时一次、每周一次、每 5 分钟一次)

在包含要执行的代码的方法定义之前使用 @Cron() 装饰器声明一个 cron 工作,如下所示:

import { Injectable, Logger } from '@nestjs/common';
import { Cron } from '@nestjs/schedule';

@Injectable()
export class TasksService {
  private readonly logger = new Logger(TasksService.name);

  @Cron('45 * * * * *')
  handleCron() {
    this.logger.debug('Called when the current second is 45');
  }
}

在此示例中,每次当前秒为 45 时都会调用 handleCron() 方法。换句话说,该方法将在 45 秒标记处每分钟运行一次

@Cron() 装饰器支持所有标准的 定时模式

  • 星号(例如 *)
  • 作用域(例如 1-3,5)
  • 步骤(例如 */2)

在上面的示例中,将 45 * * * * * 传递给装饰器。以下键显示了如何解释 cron 模式字符串中的每个位置:

* * * * * *
| | | | | |
| | | | | day of week
| | | | months
| | | day of month
| | hours
| minutes
seconds (optional)

一些示例 cron 模式是:

* * * * * *每一秒
XSPACE45 * * * * *每分钟,第 45 秒
XSPACE0 10 * * * *每小时,第 10 分钟开始时
XSPACE0 */30 9-17 * * *上午 9 点到下午 5 点之间每 30 分钟一班
XSPACE0 30 11 * * 1-5周一至周五上午 11:30

@nestjs/schedule 包提供了一个方便的枚举,其中包含常用的 cron 模式。可以按如下方式使用此枚举:

import { Injectable, Logger } from '@nestjs/common';
import { Cron, CronExpression } from '@nestjs/schedule';

@Injectable()
export class TasksService {
  private readonly logger = new Logger(TasksService.name);

  @Cron(CronExpression.EVERY_30_SECONDS)
  handleCron() {
    this.logger.debug('Called every 30 seconds');
  }
}

在此示例中,handleCron() 方法将每 30 秒调用一次

或者,可以向 @Cron() 装饰器提供一个 JavaScript Date 对象。这样做会导致工作在指定日期恰好执行一次

提示:使用 JavaScript 日期算术来安排相对于当前日期的工作。例如,@Cron(new Date(Date.now() + 10 * 1000))安排工作在应用启动后 10 秒运行

此外,可以提供其他选项作为 @Cron() 装饰器的第二个参数

name用于在声明后访问和控制 cron 工作。
timeZone指定执行的时区。这将修改相对于你的时区的实际时间。如果时区无效,则会引发错误。可以在 时刻时区 网站上查看所有可用的时区
utcOffset这允许指定时区的偏移量,而不是使用 timeZone 默认参数
disabled这表明工作是否将完全执行
import { Injectable } from '@nestjs/common';
import { Cron, CronExpression } from '@nestjs/schedule';

@Injectable()
export class NotificationService {
  @Cron('* * 0 * * *', {
    name: 'notifications',
    timeZone: 'Europe/Paris',
  })
  triggerNotifications() {}
}

可以在声明后访问和控制 cron 工作,或使用动态 API 动态创建 cron 工作(其 cron 模式在运行时定义)。要通过 API 访问声明性 cron 工作,必须通过将可选选项对象中的 name 属性作为装饰器的第二个参数传递,将工作与名称相关联

声明间隔

要声明方法应以(重复的)指定间隔运行,请在方法定义前加上 @Interval() 装饰器。将间隔值作为以毫秒为单位的数字传递给装饰器,如下所示:

@Interval(10000)
handleInterval() {
  this.logger.debug('Called every 10 seconds');
}

提示:该机制在底层使用了 JavaScript setInterval() 函数。还可以利用 cron 工作来安排重复工作

如果要通过动态 API从声明类外部控制声明间隔,请使用以下结构将间隔与名称相关联:

@Interval('notifications', 2500)
handleInterval() {}

动态 API 还可以创建动态间隔,其中间隔的属性是在运行时定义的,并可以列出和删除它们

声明超时

要声明方法应在指定的超时时间运行(一次) ,请在方法定义前加上 @Timeout() 装饰器。将应用启动的相对时间偏移量(以毫秒为单位)传递给装饰器,如下所示:

@Timeout(5000)
handleTimeout() {
  this.logger.debug('Called once after 5 seconds');
}

提示:该机制在底层使用了 JavaScript setTimeout() 函数

如果要通过 动态 API 从声明类外部控制声明超时,请使用以下结构将超时与名称相关联:

@Timeout('notifications', 2500)
handleTimeout() {}

动态 API 还支持创建动态超时,其中超时的属性在运行时定义,并列出和删除它们

动态调度模块 API

@nestjs/schedule 模块提供了一个动态 API,可以管理声明式定时任务timeoutsintervals。该 API 还支持创建和管理动态 cron 工作、超时和间隔,其中属性在运行时定义

动态 cron 工作

使用 SchedulerRegistry API 从代码中的任何位置按名称获取对 CronJob 实例的引用。首先,使用标准构造函数注入注入 SchedulerRegistry

constructor(private schedulerRegistry: SchedulerRegistry) {}

提示:从 @nestjs/schedule 包导入 SchedulerRegistry

然后在类中使用它,如下所示。假设使用以下声明创建了一个 cron 工作:

@Cron('* * 8 * * *', {
  name: 'notifications',
})
triggerNotifications() {}

使用以下内容访问此工作:

const job = this.schedulerRegistry.getCronJob('notifications');

job.stop();
console.log(job.lastDate());

getCronJob() 方法返回指定的 cron 工作。返回的 CronJob 对象有以下方法:

stop()停止计划运行的工作
start()重新启动已停止的工作
setTime(time: CronTime)停止工作,为其设置新时间,然后启动它
lastDate()返回上次执行工作发生的日期的 DateTime 表示形式
nextDate()返回 DateTime 表示的日期,表示计划下次执行工作的日期
nextDates(count: number)为将触发工作执行的下一组日期提供 DateTime 表示形式的数组(大小 count)。count 默认为 0,返回空数组

提示:在 DateTime 对象上使用 toJSDate() 将它们渲染为与此 DateTime 等效的 JavaScript Date

使用 SchedulerRegistry#addCronJob 方法动态创建一个新的 cron 工作,如下:

addCronJob(name: string, seconds: string) {
  const job = new CronJob(`${seconds} * * * * *`, () => {
    this.logger.warn(`time (${seconds}) for job ${name} to run!`);
  });

  this.schedulerRegistry.addCronJob(name, job);
  job.start();

  this.logger.warn(
    `job ${name} added for each minute at ${seconds} seconds!`,
  );
}

在此代码中,使用 cron 包中的 CronJob 对象来创建 cron 工作。CronJob 构造函数将 cron 模式(就像 @Cron() decorator 一样)作为其第一个参数,并将在 cron 计时器触发时执行的回调作为其第二个参数。SchedulerRegistry#addCronJob 方法有两个参数:CronJob 和 CronJob 对象本身的名称

警告:请记住在访问之前注入 SchedulerRegistry。从 cron 包中导入 CronJob

使用 SchedulerRegistry#deleteCronJob 方法删除一个命名的 cron 工作,如下:

deleteCron(name: string) {
  this.schedulerRegistry.deleteCronJob(name);
  this.logger.warn(`job ${name} deleted!`);
}

使用 SchedulerRegistry#getCronJobs 方法列出所有 cron 工作,如下所示:

getCrons() {
  const jobs = this.schedulerRegistry.getCronJobs();
  jobs.forEach((value, key, map) => {
    let next;
    try {
      next = value.nextDate().toJSDate();
    } catch (e) {
      next = 'error: next fire date is in the past!';
    }
    this.logger.log(`job: ${key} -> next: ${next}`);
  });
}

getCronJobs() 方法返回 map。在此代码中,遍历映射并尝试访问每个 CronJob 的 nextDate() 方法。在 CronJob API 中,如果工作已经触发并且没有未来的触发日期,则会引发异常

动态间隔

使用 SchedulerRegistry#getInterval 方法获取对区间的引用。如上所述,使用标准构造函数注入注入 SchedulerRegistry

constructor(private schedulerRegistry: SchedulerRegistry) {}

并按如下方式使用它:

const interval = this.schedulerRegistry.getInterval('notifications');
clearInterval(interval);

使用 SchedulerRegistry#addInterval 方法动态创建一个新的区间,如下:

addInterval(name: string, milliseconds: number) {
  const callback = () => {
    this.logger.warn(`Interval ${name} executing at time (${milliseconds})!`);
  };

  const interval = setInterval(callback, milliseconds);
  this.schedulerRegistry.addInterval(name, interval);
}

在这段代码中,创建了一个标准的 JavaScript 区间,然后将它传递给 SchedulerRegistry#addInterval 方法。该方法有两个参数:间隔的名称,以及间隔本身

使用 SchedulerRegistry#deleteInterval方法删除命名区间,如下:

deleteInterval(name: string) {
  this.schedulerRegistry.deleteInterval(name);
  this.logger.warn(`Interval ${name} deleted!`);
}

使用 SchedulerRegistry#getIntervals 方法列出所有区间,如下所示:

getIntervals() {
  const intervals = this.schedulerRegistry.getIntervals();
  intervals.forEach(key => this.logger.log(`Interval: ${key}`));
}

动态超时

使用 SchedulerRegistry#getTimeout 方法获取对超时的引用。如上所述,使用标准构造函数注入注入 SchedulerRegistry

constructor(private readonly schedulerRegistry: SchedulerRegistry) {}

并按如下方式使用它:

const timeout = this.schedulerRegistry.getTimeout('notifications');
clearTimeout(timeout);

使用 SchedulerRegistry#addTimeout 方法动态创建一个新的超时,如下:

addTimeout(name: string, milliseconds: number) {
  const callback = () => {
    this.logger.warn(`Timeout ${name} executing after (${milliseconds})!`);
  };

  const timeout = setTimeout(callback, milliseconds);
  this.schedulerRegistry.addTimeout(name, timeout);
}

在这段代码中,创建了一个标准的 JavaScript 超时,然后将其传递给 SchedulerRegistry#addTimeout 方法。该方法有两个参数:超时的名称,以及超时本身

使用 SchedulerRegistry#deleteTimeout 方法删除命名超时,如下:

deleteTimeout(name: string) {
  this.schedulerRegistry.deleteTimeout(name);
  this.logger.warn(`Timeout ${name} deleted!`);
}

使用 SchedulerRegistry#getTimeouts 方法列出所有超时,如下所示:

getTimeouts() {
  const timeouts = this.schedulerRegistry.getTimeouts();
  timeouts.forEach(key => this.logger.log(`Timeout: ${key}`));
}