任务调度
任务调度允许你安排任意代码(方法/函数)在固定的日期/时间、循环间隔或指定间隔后执行一次。在 Linux 世界中,这通常由操作系统级别的 cron 等包处理。对于 Node.js 应用,有几个模拟 cron 类功能的包。Nest 提供了 @nestjs/schedule 包,它与流行的 Node.js cron 包集成。我们将在本章中介绍此包。
安装
要开始使用它,我们首先安装所需的依赖。
$ 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 作业、超时和间隔。注册发生在 onApplicationBootstrap 生命周期钩子触发时,确保所有模块都已加载并声明了任何计划的作业。
声明式 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');
}
}在此示例中,handleCron() 方法将在每次当前秒为 45 时被调用。换句话说,该方法将每分钟运行一次,在第 45 秒时执行。
@Cron() 装饰器支持以下标准 cron 模式:
- 星号(例如
*) - 范围(例如
1-3,5) - 步长(例如
*/2)
在上面的示例中,我们将 45 * * * * * 传递给装饰器。以下是 cron 模式字符串中每个位置的解释:
* * * * * *
| | | | | |
| | | | | 星期几
| | | | 月份
| | | 月份中的第几天
| | 小时
| 分钟
秒(可选)一些示例 cron 模式:
| 模式 | 描述 |
|---|---|
* * * * * * | 每秒 |
45 * * * * * | 每分钟,在第 45 秒 |
0 10 * * * * | 每小时,在第 10 分钟开始时 |
0 */30 9-17 * * * | 上午 9 点到下午 5 点之间每 30 分钟 |
0 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() 注解的方法都会自动包装在 try-catch 块中。
或者,你可以向 @Cron() 装饰器提供一个 JavaScript Date 对象。这样做会使作业在指定日期恰好执行一次。
提示
使用 JavaScript 日期运算来安排相对于当前日期的作业。例如,@Cron(new Date(Date.now() + 10 * 1000)) 可以安排一个在应用启动 10 秒后运行的作业。
此外,你可以将附加选项作为第二个参数提供给 @Cron() 装饰器。
| 选项 | 描述 |
|---|---|
name | 用于在声明后访问和控制 cron 作业。 |
timeZone | 指定执行的时区。这将修改相对于你的时区的实际时间。如果时区无效,将抛出错误。你可以在 Moment Timezone 网站查看所有可用的时区。 |
utcOffset | 这允许你指定时区的偏移量,而不是使用 timeZone 参数。 |
waitForCompletion | 如果为 true,在当前 onTick 回调完成之前,不会运行 cron 作业的其他实例。在当前 cron 作业运行时发生的任何新的计划执行将被完全跳过。 |
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() {}如果发生异常,它将被记录到控制台,因为每个使用 @Interval() 注解的方法都会自动包装在 try-catch 块中。
动态 API 还允许创建动态间隔(其属性在运行时定义),以及列出和删除它们。
声明式超时
要声明方法应在指定超时后运行(一次),请在方法定义前加上 @Timeout() 装饰器。将相对于应用启动的时间偏移量(以毫秒为单位)传递给装饰器,如下所示:
@Timeout(5000)
handleTimeout() {
this.logger.debug('Called once after 5 seconds');
}提示
此机制在底层使用 JavaScript setTimeout() 函数。
如果发生异常,它将被记录到控制台,因为每个使用 @Timeout() 注解的方法都会自动包装在 try-catch 块中。
如果你想通过动态 API 从声明类外部控制声明式超时,请使用以下构造将超时与名称关联:
@Timeout('notifications', 2500)
handleTimeout() {}动态 API 还允许创建动态超时(其属性在运行时定义),以及列出和删除它们。
动态调度模块 API
@nestjs/schedule 模块提供了一个动态 API,用于管理声明式 cron 作业、超时和间隔。该 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() 装饰器一样)作为第一个参数,以及一个在 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。在此代码中,我们遍历 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}`));
}示例
一个可用的示例在这里。