本文正在参与「金石方案 . 分割6万现金大奖」
Cron表达式是用来表达时刻相关信息的字符串,用来做守时使命这类需求是最好的挑选,前端在浏览器端不太会用得到,但假如是node.js相关的业务,这便是一个必备的技术,学好cron表达式,再结合一些东西库(node-schedule),你就能够轻松写好一切场景的守时使命。
Cron表达式
Cron其实是类Unix系统中的使命调度程序,以固守时刻、日期或间隔守时履行脚本使命,最适合来组织重复性的使命,比如守时发送邮件,监控数据指标等。
冷知识:Cron的姓名来源于chronos,希腊语中的时刻
守时使命必定得结合场景,下面便是一个简略的比如
场景:周五晚上10点提醒我发周报
Cron表达式:0 0 22 ? * 6
上面的表达式看不懂不要紧,下面就来详细解说每个字符是什么意思
字符意义
年用的不多,就没有展示
如上图所示,Cron表达式的字符串是由简略的几个字符和空格拼接而成,一般会有4-6个空格,空格隔开的那部分就会形成时刻子装备。
子装备 | 是否必填 | 规模 | 允许的特别字符 | 阐明 |
---|---|---|---|---|
秒 | 否 | 0-59 |
* , - /
|
不常用 |
分 | 是 | 0-59 |
* , - /
|
|
小时 | 是 | 0-23 |
* , - /
|
|
几号 | 是 | 1-31 |
* , - / ? L W
|
|
月份 | 是 | 1-12或JAN – DEC |
* , - /
|
|
星期几 | 是 | 0-7或SUN-SAT |
* , - / ? L #
|
0和7是周六,1是周日 |
年 | 否 | 1970-2099 |
* , - /
|
不常用 |
特别字符阐明:
*
:匹配恣意单个原子值,每分钟,每小时,每天等
,
:分隔列表项,星期项 2,4,6
表明每周的一三五匹配
-
:界说规模,小时项 9-18
表明早上9点到下午6点,每小时都匹配
/
:表明间隔时刻履行,小时项9/2
表明从早上9点开端,每2小时履行一次
?
:只用于日期项和星期项,为了表明互斥,日期项假如有值,星期项就得用?,反之亦然,否则装备会失效
L
:只用于日期项和星期项,表明一个月的倒数第几天或一星期中的倒数第几天,5L
表明倒数第五天
W
:只用于日期项,表明距离作业日(周一到周五)最近的一天
#
:只用于星期项,5#3
对应于每个月的第三个星期五
常见场景
每天早上8点签到抽奖
0 0 8 * * ?
一三五该锻炼身体了
0 30 19 ? * 2,4,6
作业2小时,摸鱼20分钟
0 0 9/2 ? * MON-FRI
月末写总结
0 15 10 L * ?
双11尾款人冲鸭
59 59 23 10 11 ?
node-schedule
node.js并没有原生的办法支持调用cron,需要借助第三方库来完成,首要仍是依赖于setTimeout
来完成。
node-schedule
是一个灵敏时刻调度东西库,支持cron表达式,一般Date目标,内置语义化类等时刻格局
装置:
npm install node-schedule
// or
yarn add node-schedule
时刻规矩
node-schedule
提供scheduleJon
办法来注册调度使命,第一个参数是时刻规矩,第二个参数是时刻匹配后的回调
cron
表达式中的L和W暂不支持
固守时刻(只会履行一次)
语义化时刻实例
这是node-schedule
内置的语义化时刻规矩RecurrenceRule
,装备起来更加明晰,能够避免由于目炫而导致cron表达式配错
属性如下所示:
second (0-59)
minute (0-59)
hour (0-23)
date (1-31)
month (0-11)
year
dayOfWeek (0-6) Starting with Sunday
tz
tz – time zone时区,默认系统时区
界说规模
上面的代码就界说了规矩收效的规模,在未来5秒内,每秒都履行
Job工作
经过上面的简略运用介绍,能够看到每次调用scheduleJob函数,都会回来一个job实例,这儿来讲讲job相关的工作处理
job.cancel(reschedule = false)
撤销当时job一切守时使命,入参是一个布尔值,true表明清空后从头开启当时守时使命,默认为false
job.cancelNext(reschedule = true)
和cancel工作类似,可是这是撤销下一次即将履行的函数,reschedule默认为true
job.reschedule(spec)
对job从头界说时刻规矩
job.nextInvocation()
手动调用最近一次时刻匹配的触发器
Job
还基于EventEmitter
来完成了几个通用的工作监听,简略用法如下:
run:表明当时使命正在履行
success:会回来invoke成果,支持异步Promise语法
error:异步Promise报错才会有回来成果
scheduled:使命注册或从头注册实施
cancel:使命撤销时履行
不了解EventEmitter
,能够看下简易版别的nanoevent
,逻辑相当好了解
export let createNanoEvents = () => ({
events: {},
emit(event, ...args) {
let callbacks = this.events[event] || []
for (let i = 0, length = callbacks.length; i < length; i++) {
callbacks[i](...args)
}
},
on(event, cb) {
this.events[event]?.push(cb) || (this.events[event] = [cb])
return () => {
this.events[event] = this.events[event]?.filter(i => cb !== i)
}
}
})
自界说封装
Job的使命注册和工作监听其实是分开用的,细心想想能不能把两者结合起来,整组成一个Class类,所以我封装了下面这个Schedule类:
class Schedule {
constructor() {
if (!this.spec) {
throw new Error('Time config (spec) Required')
}
const job = schedule.scheduleJob(this.spec, this.invoke)
this._registerEvent(job)
this.job = job
}
async invoke() {}
_registerEvent(job) {
job.on('success', (val) => {
this._log('success', val)
this.successCb(val)
})
job.on('error', (err) => {
this._log('error', err)
this.errorCb(err)
})
}
// 成功的回调
successCb() {}
// 失利的回调
errorCb() {}
// 可自界说封装其他
_log(type, info) {
if (type === 'error') {
// console.error(info)
return
}
// console.log(type, info)
}
}
具体用法:
class SendEmail extends Schedule {
get spec() {
return '0/5 * * * * *'
}
async invoke() {
const res = await mockSms(1)
return res
}
errorCb() {
// retry
mockSms(1)
}
}
new SendEmail()
装备时刻规矩,回调,错误处理就能在一个类里面处理。
同时,你还能够在Schedule里统一打你的大局日志,不漏掉任何调用信息:
源码
其实node-schedule
全体的逻辑代码并不多,只要500行左右,所以也十分值得花点小时刻研究一下,下面是我画的一张依赖引证相关图
虚线其实便是中心办法scheduleJob
的调用链路,入口代码也很好了解,其实便是判断下相关的参数,然后实例化Job类,从而调用schedule进行注册守时使命:
function scheduleJob() {
if (arguments.length < 2) {
throw new RangeError('Invalid number of arguments');
}
const name = (arguments.length >= 3 && typeof arguments[0] === 'string') ? arguments[0] : null;
const spec = name ? arguments[1] : arguments[0];
const method = name ? arguments[2] : arguments[1];
const callback = name ? arguments[3] : arguments[2];
if (typeof method !== 'function') {
throw new RangeError('The job method must be a function.');
}
// 校验完入参后,开端构造使命单元
const job = new Job(name, method, callback);
if (job.schedule(spec)) {
return job;
}
return null;
}
使命注册器 – Job
首要处理以下工作:
-
解析时刻相关装备(
cron
,Date
,RecurrenceRule
) -
界说Job独有的工作(cancel,cancelNext,reschedule等)
-
分配invoke办法
解析时刻装备:
-
start,end都会在这个阶段装备
-
用
cron-parser
解析cron表达式,解析生成就开端参加下一次的行列调用(scheduleNextRecurrence
),回来的invocation
参加job
的pendingInvocations
-
cron解析失利,判断是否是
Date
格局,这种直接实例化Invocation
,并参加等待 -
目标就解析成
RecurrenceRule
装备,剩余逻辑和流程2相同
cron-parser
能够了解为cron表达式的解析东西包,便利提取每次的收效时刻
const cronParser = require('cron-parser')
const CronDate = require('cron-parser/lib/date')
Job.prototype.schedule = function(spec) {
const self = this;
let success = false;
let inv;
let start;
let end;
let tz;
// save passed-in value before 'spec' is replaced
// 记载当时的入参解析,在传入之前
if (typeof spec === 'object' && 'tz' in spec) {
tz = spec.tz;
}
if (typeof spec === 'object' && spec.rule) {
start = spec.start || undefined;
end = spec.end || undefined;
spec = spec.rule;
if (start) {
if (!(start instanceof Date)) {
start = new Date(start);
}
start = new CronDate(start, tz);
if (!isValidDate(start) || start.getTime() < Date.now()) {
start = undefined;
}
}
if (end && !(end instanceof Date) && !isValidDate(end = new Date(end))) {
end = undefined;
}
if (end) {
end = new CronDate(end, tz);
}
}
try {
// 1. 解析cron表达式
const res = cronParser.parseExpression(spec, {currentDate: start, tz: tz});
// 回来下一次的调度办法
inv = scheduleNextRecurrence(res, self, start, end);
if (inv !== null) {
success = self.trackInvocation(inv);
}
} catch (err) {
const type = typeof spec;
// 2. 字符串或数字判断为固守时刻 Date 格局
if ((type === 'string') || (type === 'number')) {
spec = new Date(spec);
}
// 2. 标准 Date 格局,手动注册Invocation目标
if ((spec instanceof Date) && (isValidDate(spec))) {
spec = new CronDate(spec);
self.isOneTimeJob = true;
if (spec.getTime() >= Date.now()) {
inv = new Invocation(self, spec);
scheduleInvocation(inv);
success = self.trackInvocation(inv);
}
// 3. 假如是目标,转换成RecurrenceRule类
} else if (type === 'object') {
self.isOneTimeJob = false;
if (!(spec instanceof RecurrenceRule)) {
const r = new RecurrenceRule();
if ('year' in spec) {
r.year = spec.year;
}
if ('month' in spec) {
r.month = spec.month;
}
if ('date' in spec) {
r.date = spec.date;
}
if ('dayOfWeek' in spec) {
r.dayOfWeek = spec.dayOfWeek;
}
if ('hour' in spec) {
r.hour = spec.hour;
}
if ('minute' in spec) {
r.minute = spec.minute;
}
if ('second' in spec) {
r.second = spec.second;
}
spec = r;
}
spec.tz = tz;
inv = scheduleNextRecurrence(spec, self, start, end);
if (inv !== null) {
success = self.trackInvocation(inv);
}
}
}
scheduledJobs[this.name] = this;
return success;
};
Job独有的工作:
这儿讲个比较有代表性的cancel工作
由于一个Job可能会屡次调用schedule办法,那么就会有好几个不同的时刻装备,这时候其实便是用行列(上文有说到的pendingInvocations
)往来不断办理,假如要撤销一切守时使命,那么就要遍历撤销一切等待中的Invocation
实例,进行撤销。
cancelInvocation
办法会在下文说到
// 撤销使命
this.cancel = function(reschedule) {
reschedule = (typeof reschedule == 'boolean') ? reschedule : false;
let inv, newInv;
const newInvs = [];
for (let j = 0; j < this.pendingInvocations.length; j++) {
inv = this.pendingInvocations[j];
cancelInvocation(inv);
if (reschedule && (inv.recurrenceRule.recurs || inv.recurrenceRule.next)) {
newInv = scheduleNextRecurrence(inv.recurrenceRule, this, inv.fireDate, inv.endDate);
if (newInv !== null) {
newInvs.push(newInv);
}
}
}
this.pendingInvocations = [];
for (let k = 0; k < newInvs.length; k++) {
this.trackInvocation(newInvs[k]);
}
// remove from scheduledJobs if reschedule === false
if (!reschedule) {
this.deleteFromSchedule()
}
return true;
};
分配invoke办法:
Job.prototype.invoke = function(fireDate) {
// trigger计数用于单测,无实际业务逻辑
this.setTriggeredJobs(this.triggeredJobs() + 1);
return this.job(fireDate);
};
守时使命履行 – Invocation
中心逻辑首要处理以下工作:
- 守时器适配守时使命
- job工作emit触发
守时器闭环:
这儿其实便是守时使命的中心部分了,简略来讲便是利用setTimeout来完成,经过下面的流程图其实你就能大概知道怎么完成守时使命
带着上面这个图看源码:
上文中解析完cron装备后,就会履行scheduleNextRecurrence
:
- 判断当时时刻处于合理的规模内,[start, end]中
- 实例化
Invocation
,并组织守时使命 =>scheduleInvocation
function scheduleNextRecurrence(rule, job, prevDate, endDate) {
// 没有指定的开端时刻,当时时刻便是开端时刻
prevDate = (prevDate instanceof CronDate) ? prevDate : new CronDate();
// 回来规矩的下一次履行时刻
const date = (rule instanceof RecurrenceRule) ? rule._nextInvocationDate(prevDate) : rule.next();
if (date === null) {
return null;
}
// 假如下次履行时刻超出完毕时刻,就直接完毕
if ((endDate instanceof CronDate) && date.getTime() > endDate.getTime()) {
return null;
}
// 实例化Invocation, 并开端组织守时使命
const inv = new Invocation(job, date, rule, endDate);
scheduleInvocation(inv);
return inv;
}
scheduleInvocation 组织调度器:
- 依据cron表达式解析的最近时刻排序当时的invocation
- job 触发
scheduled
工作 - 下一层调用链 =>
prepareNextInvocation
function sorter(a, b) {
return (a.fireDate.getTime() - b.fireDate.getTime());
}
// 组织当时的调度器,按时刻排序,event触发scheduled工作
function scheduleInvocation(invocation) {
sorted.add(invocations, invocation, sorter);
prepareNextInvocation();
const date = invocation.fireDate instanceof CronDate ? invocation.fireDate.toDate() : invocation.fireDate;
invocation.job.emit('scheduled', date);
}
prepareNextInvocation 设置守时器:
- 铲除当时已有守时器 – 同一时刻下只能有一个setTimeout存在
- 守时器触发后就履行invoke
- 触发job的run,success,error工作
- 铲除行列,并准备下一次的守时器 (prepareNextInvocation),有点递归的意思
function prepareNextInvocation() {
if (invocations.length > 0 && currentInvocation !== invocations[0]) {
// 铲除当时守时器
if (currentInvocation !== null) {
lt.clearTimeout(currentInvocation.timerID);
currentInvocation.timerID = null;
currentInvocation = null;
}
currentInvocation = invocations[0];
const job = currentInvocation.job;
const cinv = currentInvocation;
currentInvocation.timerID = runOnDate(currentInvocation.fireDate, function() {
currentInvocationFinished();
if (job.callback) {
job.callback();
}
console.log(cinv.recurrenceRule.recurs, cinv.recurrenceRule._endDate)
// 把新cron解析的办法继续推进去
if (cinv.recurrenceRule.recurs || cinv.recurrenceRule._endDate === null) {
const inv = scheduleNextRecurrence(cinv.recurrenceRule, cinv.job, cinv.fireDate, cinv.endDate);
if (inv !== null) {
inv.job.trackInvocation(inv);
}
}
job.stopTrackingInvocation(cinv);
try {
const result = job.invoke(cinv.fireDate instanceof CronDate ? cinv.fireDate.toDate() : cinv.fireDate);
job.emit('run');
job.running += 1;
// invoke函数可能是异步的,这儿会判断是否成功履行
if (result instanceof Promise) {
result.then(function (value) {
job.emit('success', value);
job.running -= 1;
}).catch(function (err) {
job.emit('error', err);
job.running -= 1;
});
} else {
job.emit('success', result);
job.running -= 1;
}
} catch (err) {
job.emit('error', err);
job.running -= 1;
}
// 假如是单次时刻调用,就直接删除Job
if (job.isOneTimeJob) {
job.deleteFromSchedule();
}
});
}
}
// 中心基础代码,设置守时器
function runOnDate(date, job) {
const now = Date.now();
const then = date.getTime();
return lt.setTimeout(function() {
if (then > Date.now())
runOnDate(date, job);
else
job();
}, (then < now ? 0 : then - now));
}
// 铲除当时的行列,准备下一次的调用
function currentInvocationFinished() {
invocations.shift();
currentInvocation = null;
prepareNextInvocation();
}
这儿还有个细节
lt.setTimeout
这个其实是引了一个long-timeout
的库,首要是为了处理js中最大安全数字的问题:
至此,守时使命就形成了闭环。
完毕
虽说守时使命一般是后端来做,可是作为前端,学习一下其中的精华仍是不错的,一些简略的场景也能用node-schedule来掩盖。
发明不易,希望jym多多 点赞 + 关注 + 收藏 三连,继续更新中!!!
PS: 文中有任何错误,欢迎掘友纠正
往期精彩
-
前端东西小攻略之Prettier✨
-
React调试利器:React DevTools ✨
-
怎么打造属于自己的CLI东西 ✨
参阅:
github.com/node-schedu…
www.npmjs.com/package/lon…
github.com/ai/nanoeven…