利用 cron 实现 Node.js 的定时任务

一般的「定时任务」分两种,第一种是「间隔 xx 时间执行」,第二种是「在每x月/日/小时/分钟/秒」执行,又叫「周期任务」。前者使用 setInterval 比较容易做到,而后者就麻烦了。

我们可以使用 Node.js 的 cron 库来实现。

安装

1
2
3
4
5
6
7
8
# npm
npm i cron

# yarn
yarn add cron

# pnpm
pnpm i cron

原理

cron 使用 cron 表达式(维基百科百度百科),所以需要对它的语法有一定的了解,下面简单叙述:

如果你不想看,有在线工具可以帮你生成 cron 表达式。

Cron 表达式是一个具有时间含义的字符串,字符串以 5 个空格隔开,分为 6 个域,格式为X X X X X X。其中 X 是一个域的占位符。单个域有多个取值时,使用半角逗号 , 隔开取值。每个域可以是确定的取值,也可以是具有逻辑意义的特殊字符。

这 6 个域分别为秒、分钟、小时、星期、月份和星期,具体含义如下:

取值范围 特殊字符
[0, 59] * , - /
分钟 [0, 59] * , - /
小时 [0, 23] * , - /
日期 [1, 31] * , - / ? L W
月份 [1, 12][JAN, DEC] * , - /
星期 [1, 7](1代表星期一,7代表星期日)或 [MON, SUN] * , - / ? L #

除了上面的取值范围 ,每个域还支持一些特殊字符,它们的含义如下:

特殊字符 含义 示例
* 所有可能的值 在月域中,* 表示每个月;在星期域中,* 表示星期的每一天。
, 列出枚举值 在分钟域中,5,20 表示分别在 5 分钟和 20 分钟触发一次。
- 范围。 在分钟域中,5-20 表示从 5 分钟到 20 分钟之间每隔一分钟触发一次。
/ 指定数值的增量。 在分钟域中,0/15 表示从第 0分钟开始,每 15 分钟。在分钟域中 3/20 表示从第 3 分钟开始,每 20 分钟。
? 不指定值,仅日期和星期域支持该字符。 当日期或星期域其中之一被指定了值以后,为了避免冲突,需要将另一个域的值设为 ?
L 单词 Last 的首字母,表示最后一天,仅日期和星期域支持该字符。
说明 指定L字符时,避免指定列表或者范围,否则,会导致逻辑问题。
在日期域中,L表示某个月的最后一天。在星期域中,L表示一个星期的最后一天,也就是星期日(SUN)。
如果在L前有具体的内容,例如,在星期域中的6L表示这个月的最后一个星期六。
W 除周末以外的有效工作日,在离指定日期的最近的有效工作日触发事件。W字符寻找最近有效工作日时不会跨过当前月份,连用字符LW时表示为指定月份的最后一个工作日。 在日期域中5W,如果5日是星期六,则将在最近的工作日星期五,即4日触发。如果5日是星期天,则将在最近的工作日星期一,即6日触发;如果5日在星期一到星期五中的一天,则就在5日触发。
# 确定每个月第几个星期几,仅星期域支持该字符。 在星期域中,4#2 表示某月的第二个星期四。

下面是一些例子:

示例 说明
0 15 10 ? * * 每天上午 10:15 执行任务
0 15 10 * * ? 每天上午 10:15 执行任务
0 0 12 * * ? 每天中午 12:00 执行任务
0 0 10,14,16 * * ? 每天上午 10:00 点、下午 14:00 以及下午 16:00 执行任务
0 0/30 9-17 * * ? 每天上午 09:00 到下午 17:00 时间段内每隔半小时执行任务
0 * 14 * * ? 每天下午 14:00 到下午 14:59 时间段内每隔 1 分钟执行任务
0 0-5 14 * * ? 每天下午 14:00 到下午 14:05 时间段内每隔 1 分钟执行任务
0 0/5 14 * * ? 每天下午 14:00 到下午 14:55 时间段内每隔 5 分钟执行任务
0 0/5 14,18 * * ? 每天下午 14:00 到下午 14:55、下午 18:00 到下午 18:55 时间段内每隔 5 分钟执行任务
0 0 12 ? * WED 每个星期三中午 12:00 执行任务
0 15 10 15 * ? 每月 15 日上午 10:15 执行任务
0 15 10 L * ? 每月最后一日上午 10:15 执行任务
0 15 10 ? * 6L 每月最后一个星期六上午 10:15 执行任务
0 15 10 ? * 6#3 每月第三个星期六上午 10:1 5执行任务
0 10,44 14 ? 3 WED 每年3月的每个星期三下午 14:10 和 14:44 执行任务

用法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
// 使用 JavaScript 的自行替换成 require
import { CronJob } from 'cron';

// 定义任务
const job = new CronJob(
// cronTime:必填,任务的触发时机,可以传一个 cron 表达式或者 Date 对象
'* * * * * *',

// onTick:必填,任务的执行函数
() => { console.log('这个任务将会在每秒钟输出这句话') },

// onComplete:可选,调用 job.stop() 停止任务时触发
null,

// start:可选,任务是否立即执行,默认值为 false,如果它为 false,需要手动调用 job.start() 方法
true,

// timeZone:可选,执行任务的时区,其值可参考 https://momentjs.com/timezone/
'America/Los_Angeles',

// context:可选,执行 onTick 方法的上下文,如果 onTick 函数里使用了 `this`,可能需要设置此项
null,

// runOnInit:可选,当 onTick 第一次执行时触发的函数,可以设置为 `false`
false,

// utcOffset:可选,设置时区偏移量,会与 `timeZone` 发生冲突

// unrefTimeout:可选
);

// 启动任务
job.start();

// 停止任务
job.stop();

// 停止任务,并更改该任务的 cronTime
job.setTime('* * * * * *');

// 任务的最后一次执行时间
job.lastDate

// 任务的下一次执行时间
job.nextDates

// 任务是否在执行
job.running

// 添加 onTick 回调
job.addCallback(() => { /* ... */ })

例子

一个朋友需要写一个定时任务,在每天的上午 10 点往群里发消息,就可以这么实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import { CronJob } from 'corn';
import Bot from './bot';

const bot = new Bot();

const job = new CronJob('* * 10 * * *', () => {
bot.send({
msgType: 'text',
text: { content: '在干嘛' },
at: { atMobiles: ['13xxxxxxxxx', '13xxxxxxxxx'] } // 保密
});
});

job.start();