NestJS 定時(shí)任務(wù)

2023-09-08 17:50 更新

定時(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)聲明。

聲明計(jì)時(shí)工作(cron job)

一個(gè)計(jì)時(shí)工作調(diào)度任何函數(shù)(方法調(diào)用)以自動(dòng)運(yùn)行, 計(jì)時(shí)工作可以:

  • 單次,在指定日期/時(shí)間
  • 重復(fù)循環(huán):重復(fù)工作可以在指定周期中指定執(zhí)行(例如,每小時(shí),每周,或者每 5 分鐘)

在包含要運(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:

  • 星號(hào)通配符 (也就是 *)
  • 范圍(也就是 1-3,5)
  • 步長(zhǎng)(也就是 */2)

在上述例子中,我們給裝飾器傳遞了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í)定義,可以列出和刪除他們。

聲明延時(shí)任務(wù)

要聲明一個(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í)定義,可以列舉和刪除他們。

動(dòng)態(tài)規(guī)劃模塊 API

@nestjs/schedule模塊提供了一個(gè)支持管理聲明定時(shí)、超時(shí)和間隔任務(wù)的動(dòng)態(tài) API。該 API 也支持創(chuàng)建和管理動(dòng)態(tài)定時(shí)、超時(shí)和間隔,這些屬性在運(yùn)行時(shí)定義。

動(dòng)態(tài)定時(shí)任務(wù)

使用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ì)象:

  • stop()-停止一個(gè)按調(diào)度運(yùn)行的任務(wù)
  • start()-重啟一個(gè)停止的任務(wù)
  • setTime(time:CronTime)-停止一個(gè)任務(wù),為它設(shè)置一個(gè)新的時(shí)間,然后再啟動(dòng)它
  • lastDate()-返回一個(gè)表示工作最后執(zhí)行日期的字符串
  • nextDates(count:number)-返回一個(gè)moment對(duì)象的數(shù)組(大小count),代表即將執(zhí)行的任務(wù)日期

在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í)行的日期,將拋出異常。

動(dòng)態(tài)間隔

使用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}`));
}

動(dòng)態(tài)超時(shí)

使用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)這里。


以上內(nèi)容是否對(duì)您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號(hào)
微信公眾號(hào)

編程獅公眾號(hào)