计划任务 Task Scheduling

导读

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

安装

要开始使用它,我们首先安装所需的依赖项。

bash
$ npm install --save @nestjs/schedule

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

app.module
ts
import { Module } from '@nestjs/common'
import { ScheduleModule } from '@nestjs/schedule'

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

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

声明性 cron 作业

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

  • 一次,在指定的日期/时间。
  • 定期运行;重复作业可以在指定时间间隔内的特定时刻运行(例如,每小时一次、每周一次、每 5 分钟一次)

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

ts
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()装饰器支持以下标准 cron 模式

  • 星号(例如*
  • 范围(例如1-3,5
  • 步骤(例如*/2

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


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

Some sample cron patterns are:

* * * * * * every second
45 * * * * * every minute, on the 45th second
0 10 * * * * every hour, at the start of the 10th minute
0 */30 9-17 * * * every 30 minutes between 9am and 5pm
0 30 11 * * 1-5 Monday to Friday at 11:30am

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

ts
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 指定执行的时区。这将修改相对于您时区的实际时间。如果时区无效,则会引发错误。您可以在 [Moment Timezone](http://momentjs.com/timezone/) 网站上查看所有可用的时区。
utcOffset 这允许您指定时区的偏移量,而不是使用 timeZone 参数。
disabled 这表明该作业是否会被执行。
ts
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() 装饰器。将间隔值(以毫秒为单位的数字)传递给装饰器,如下所示:

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

此机制在底层使用 JavaScript setInterval() 函数。您还可以利用 cron 作业来安排重复作业。

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

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

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

声明性超时

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

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

此机制在底层使用 JavaScript setTimeout() 函数。

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

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

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

动态计划模块 API

@nestjs/schedule 模块提供了一个动态 API,可用于管理声明式 cron 作业超时间隔。该 API 还可用于创建和管理 动态 cron 作业、超时和间隔,其中属性是在运行时定义的。

动态 cron 作业

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

ts
constructor(private schedulerRegistry: SchedulerRegistry) {}
提示

@nestjs/schedule 包导入 SchedulerRegistry

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

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

使用以下命令访问此作业:

ts
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 日期。

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

ts
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() 装饰器)作为其第一个参数,并将 cron 计时器触发时要执行的回调作为其第二个参数。SchedulerRegistry#addCronJob 方法采用两个参数:CronJob 的名称和 CronJob 对象本身。

警告

请记住在访问 SchedulerRegistry 之前注入它。从 cron 包导入 CronJob

使用 SchedulerRegistry#deleteCronJob 方法删除命名的 cron 作业,如下所示:

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

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

ts
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。在此代码中,我们遍历该 map 并尝试访问每个 CronJobnextDate() 方法。在 CronJob API 中,如果作业已触发且没有未来触发日期,则会引发异常。

动态间隔

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

ts
constructor(private schedulerRegistry: SchedulerRegistry) {}

并按如下方式使用它:

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

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

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

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

在此代码中,我们创建一个标准 JavaScript 间隔,然后将其传递给 SchedulerRegistry#addInterval 方法。 该方法需要两个参数:间隔的名称和间隔本身。

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

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

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

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

动态超时

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

ts
constructor(private readonly schedulerRegistry: SchedulerRegistry) {}

并按如下方式使用它:

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

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

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

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

在此代码中,我们创建一个标准 JavaScript 超时,然后将其传递给 SchedulerRegistry#addTimeout 方法。 该方法接受两个参数:超时的名称和超时本身。

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

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

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

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

示例

此处 提供了一个工作示例。