定時(shí)任務(wù)允許你按照指定的日期/時(shí)間、一定時(shí)間間隔或者一定時(shí)間后單次執(zhí)行來(lái)調(diào)度(scheduling)任意代碼(方法/函數(shù))。在Linux世界中,這經(jīng)常通過(guò)操作系統(tǒng)層面的cron包等執(zhí)行。在Node.js應(yīng)用中,有幾個(gè)不同的包可以模擬 cron 包的功能。Nest 提供了@nestjs/schedule包,其集成了流行的 Node.js 的node-cron包,我們將在本章中應(yīng)用該包。
我們首先從安裝需要的依賴開(kāi)始。
$ npm install --save @nestjs/schedule
要激活工作調(diào)度,從根AppModule中導(dǎo)入ScheduleModule并運(yùn)行forRoot()靜態(tài)方法,如下:
app.module.ts
import { Module } from '@nestjs/common';
import { ScheduleModule } from '@nestjs/schedule';
@Module({
imports: [ScheduleModule.forRoot()],
})
export class AppModule {}
.forRoot()調(diào)用初始化調(diào)度器并且注冊(cè)在你應(yīng)用中任何聲明的cron jobs,timeouts和intervals。注冊(cè)開(kāi)始于onApplicationBootstrap生命周期鉤子發(fā)生時(shí),保證所有模塊都已經(jīng)載入,任何計(jì)劃工作已經(jīng)聲明。
一個(gè)計(jì)時(shí)工作調(diào)度任何函數(shù)(方法調(diào)用)以自動(dòng)運(yùn)行, 計(jì)時(shí)工作可以:
在包含要運(yùn)行代碼的方法定義前使用@Cron()裝飾器聲明一個(gè)計(jì)時(shí)工作,如下:
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');
}
}
在這個(gè)例子中,handleCron()方法將在當(dāng)前時(shí)間為45秒時(shí)定期執(zhí)行。換句話說(shuō),該方法每分鐘執(zhí)行一次,在第 45 秒執(zhí)行。
@Cron()裝飾器支持標(biāo)準(zhǔn)的cron patterns:
在上述例子中,我們給裝飾器傳遞了45 * * * * *,下列鍵展示了每個(gè)位置的計(jì)時(shí)模式字符串的意義:
* * * * * *
| | | | | |
| | | | | day of week
| | | | month
| | | day of month
| | hour
| minute
second (optional)
一些示例的計(jì)時(shí)模式包括:
名稱 | 含義 |
---|---|
* * * * * * | 每秒 |
45 * * * * * | 每分鐘第 45 秒 |
_ 10 _ * * * | 每小時(shí),從第 10 分鐘開(kāi)始 |
0 _/30 9-17 _ * * | 上午 9 點(diǎn)到下午 5 點(diǎn)之間每 30 分鐘 |
0 30 11 * * 1-5 | 周一至周五上午 11:30 |
@nestjs/schedule包提供一個(gè)方便的枚舉
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_45_SECONDS)
handleCron() {
this.logger.debug('Called every 45 seconds');
}
}
在本例中,handleCron()方法每45秒執(zhí)行一次。
可選地,你可以為將一個(gè)JavaScript的Date對(duì)象傳遞給@Cron()裝飾器。這樣做可以讓工作在指定日期執(zhí)行一次。
使用JavaScript日期算法來(lái)關(guān)聯(lián)當(dāng)前日期和計(jì)劃工作。@Cron(new Date(Date.now()+10*1000))用于在應(yīng)用啟動(dòng) 10 秒后運(yùn)行。
你可以在聲明后訪問(wèn)并控制一個(gè)定時(shí)任務(wù),或者使用動(dòng)態(tài) API動(dòng)態(tài)創(chuàng)建一個(gè)定時(shí)任務(wù)(其定時(shí)模式在運(yùn)行時(shí)定義)。要通過(guò) API 聲明定時(shí)任務(wù),你必須通過(guò)將選項(xiàng)對(duì)象中的name屬性作為可選的第二個(gè)參數(shù)傳遞給裝飾器,從而將工作和名稱聯(lián)系起來(lái)。
@Cron('* * 8 * * *', {
name: 'notifications',
})
triggerNotifications() {}
要聲明一個(gè)以一定間隔運(yùn)行的方法,使用@Interval()裝飾器前綴。以毫秒單位的number傳遞間隔值,如下:
@Interval(10000)
handleInterval() {
this.logger.debug('Called every 10 seconds');
}
本機(jī)制在底層使用JavaScript的setInterval()函數(shù)。你也可以使用定期調(diào)度工作來(lái)應(yīng)用一個(gè)定時(shí)任務(wù)。
如果你希望在聲明類之外通過(guò)動(dòng)態(tài) API控制你聲明的時(shí)間間隔。使用下列結(jié)構(gòu)將名稱與間隔關(guān)聯(lián)起來(lái)。
@Interval('notifications', 2500)
handleInterval() {}
動(dòng)態(tài) API 也支持動(dòng)態(tài)創(chuàng)建時(shí)間間隔,間隔屬性在運(yùn)行時(shí)定義,可以列出和刪除他們。
要聲明一個(gè)在指定時(shí)間后運(yùn)行(一次)的方法,使用@Timeout()裝飾器前綴。將從應(yīng)用啟動(dòng)的相關(guān)時(shí)間偏移量(毫秒)傳遞給裝飾器,如下:
@Timeout(5000)
handleTimeout() {
this.logger.debug('Called once after 5 seconds');
}
本機(jī)制在底層使用 JavaScript 的setTimeout()方法
如果你想要在聲明類之外通過(guò)動(dòng)態(tài) API 控制你聲明的超時(shí)時(shí)間,將超時(shí)時(shí)間和一個(gè)名稱以如下結(jié)構(gòu)關(guān)聯(lián):
@Timeout('notifications', 2500)
handleTimeout() {}
動(dòng)態(tài) API 同時(shí)支持創(chuàng)建動(dòng)態(tài)超時(shí)時(shí)間,超時(shí)時(shí)間在運(yùn)行時(shí)定義,可以列舉和刪除他們。
@nestjs/schedule模塊提供了一個(gè)支持管理聲明定時(shí)、超時(shí)和間隔任務(wù)的動(dòng)態(tài) API。該 API 也支持創(chuàng)建和管理動(dòng)態(tài)定時(shí)、超時(shí)和間隔,這些屬性在運(yùn)行時(shí)定義。
使用SchedulerRegistryAPI 從你代碼的任何地方獲取一個(gè)CronJob實(shí)例的引用。首先,使用標(biāo)準(zhǔn)構(gòu)造器注入ScheduleRegistry。
constructor(private schedulerRegistry: SchedulerRegistry) {}
從@nestjs/schedule包導(dǎo)入SchedulerRegistry。
使用下列類,假設(shè)通過(guò)下列定義聲明一個(gè)定時(shí)任務(wù):
@Cron('* * 8 * * *', {
name: 'notifications',
})
triggerNotifications() {}
如下獲取本工作:
const job = this.schedulerRegistry.getCronJob('notifications');
job.stop();
console.log(job.lastDate());
getCronJob()方法返回一個(gè)命名的定時(shí)任務(wù)。然后返回一個(gè)包含下列方法的CronJob對(duì)象:
在moment對(duì)象中使用toDate()來(lái)渲染成易讀的形式。
使用SchedulerRegistry.addCronJob()動(dòng)態(tài)創(chuàng)建一個(gè)新的定時(shí)任務(wù),如下:
addCronJob(name: string, seconds: string) {
const job = new CronJob(`${seconds} * * * * *`, () => {
this.logger.warn(`time (${seconds}) for job ${name} to run!`);
});
this.scheduler.addCronJob(name, job);
job.start();
this.logger.warn(
`job ${name} added for each minute at ${seconds} seconds!`,
);
}
在這個(gè)代碼中,我們使用cron包中的CronJob對(duì)象來(lái)創(chuàng)建定時(shí)任務(wù)。CronJob構(gòu)造器采用一個(gè)定時(shí)模式(類似@Cron()裝飾器作為其第一個(gè)參數(shù),以及一個(gè)將執(zhí)行的回調(diào)函數(shù)作為其第二個(gè)參數(shù)。SchedulerRegistry.addCronJob()方法有兩個(gè)參數(shù):一個(gè)CronJob名稱,以及一個(gè)CronJob對(duì)象自身。
記得在使用前注入SchedulerRegistry,從cron包中導(dǎo)入 CronJob。
使用SchedulerRegistry.deleteCronJob()方法刪除一個(gè)命名的定時(shí)任務(wù),如下:
deleteCron(name: string) {
this.scheduler.deleteCronJob(name);
this.logger.warn(`job ${name} deleted!`);
}
使用SchedulerRegistry.getCronJobs()方法列出所有定時(shí)任務(wù),如下:
getCrons() {
const jobs = this.scheduler.getCronJobs();
jobs.forEach((value, key, map) => {
let next;
try {
next = value.nextDates().toDate();
} catch (e) {
next = 'error: next fire date is in the past!';
}
this.logger.log(`job: ${key} -> next: ${next}`);
});
}
getCronJobs()方法返回一個(gè)map。在這個(gè)代碼中,我們遍歷該map并且嘗試獲取每個(gè)CronJob的nextDates()方法。在CronJobAPI 中,如果一個(gè)工作已經(jīng)執(zhí)行了并且沒(méi)有下一次執(zhí)行的日期,將拋出異常。
使用SchedulerRegistry.getInterval()方法獲取一個(gè)時(shí)間間隔的引用。如上,使用標(biāo)準(zhǔn)構(gòu)造注入SchedulerRegistry。
constructor(private schedulerRegistry: SchedulerRegistry) {}
如下使用:
const interval = this.schedulerRegistry.getInterval('notifications');
clearInterval(interval);
使用SchedulerRegistry.addInterval() 方法創(chuàng)建一個(gè)新的動(dòng)態(tài)間隔,如下:
addInterval(name: string, seconds: string) {
const callback = () => {
this.logger.warn(`Interval ${name} executing at time (${seconds})!`);
};
const interval = setInterval(callback, seconds);
this.scheduler.addInterval(name, interval);
}
在該代碼中,我們創(chuàng)建了一個(gè)標(biāo)準(zhǔn)的 JavaScript 間隔,然后將其傳遞給ScheduleRegistry.addInterval()方法。該方法包括兩個(gè)參數(shù):一個(gè)時(shí)間間隔的名稱,和時(shí)間間隔本身。
如下使用SchedulerRegistry.deleteInterval()刪除一個(gè)命名的時(shí)間間隔:
deleteInterval(name: string) {
this.scheduler.deleteInterval(name);
this.logger.warn(`Interval ${name} deleted!`);
}
使用SchedulerRegistry.getIntervals()方法如下列出所有的時(shí)間間隔:
getIntervals() {
const intervals = this.scheduler.getIntervals();
intervals.forEach(key => this.logger.log(`Interval: ${key}`));
}
使用SchedulerRegistry.getTimeout()方法獲取一個(gè)超時(shí)引用,如上,使用標(biāo)準(zhǔn)構(gòu)造注入SchedulerRegistry:
constructor(private schedulerRegistry: SchedulerRegistry) {}
并如下使用:
const timeout = this.schedulerRegistry.getTimeout('notifications');
clearTimeout(timeout);
使用SchedulerRegistry.addTimeout()方法創(chuàng)建一個(gè)新的動(dòng)態(tài)超時(shí),如下:
addTimeout(name: string, seconds: string) {
const callback = () => {
this.logger.warn(`Timeout ${name} executing after (${seconds})!`);
});
const timeout = setTimeout(callback, seconds);
this.scheduler.addTimeout(name, timeout);
}
在該代碼中,我們創(chuàng)建了個(gè)一個(gè)標(biāo)準(zhǔn)的 JavaScript 超時(shí)任務(wù),然后將其傳遞給ScheduleRegistry.addTimeout()方法,該方法包含兩個(gè)參數(shù):一個(gè)超時(shí)的名稱,以及超時(shí)對(duì)象自身。
使用SchedulerRegistry.deleteTimeout()方法刪除一個(gè)命名的超時(shí),如下:
deleteTimeout(name: string) {
this.scheduler.deleteTimeout(name);
this.logger.warn(`Timeout ${name} deleted!`);
}
使用SchedulerRegistry.getTimeouts()方法列出所有超時(shí)任務(wù):
getTimeouts() {
const timeouts = this.scheduler.getTimeouts();
timeouts.forEach(key => this.logger.log(`Timeout: ${key}`));
}
一個(gè)可用的例子見(jiàn)這里。
更多建議: