需求描述
- 日常开发中,我们常常会要执行一些定时任务
- 比如定时清理文件夹,定时发邮件等
- 本文是在node的express框架中用node-schedule这个包
- 来实现定时清理文件夹功能
node-schedule介绍
node-schedule
是一个用于在 Node.js 环境中调度和执行任务的库。- 这个包可以设置定时任务、周期性任务以及一次性任务
- 很灵活,很强大,精度高,也支持异步任务
几个简单案例
- 每天十二点,触发任务
javascript
const schedule = require('node-schedule');
// 设置一个定时任务,每天中午12点30分40秒执行
const job = schedule.scheduleJob('40 30 12 * * *', function(){
console.log('每天中午12点执行任务');
});
在这个例子中,
40 30 12 * * *'
是cron
表达式,表示每天中午12点30分40秒执行,后面三个星号*
,是占位符,表示每天、每月、每周,即常用六个星号占位,分别是:【秒 分 时 天 月 周】
- 每10秒,执行一次任务
javascript
const schedule = require('node-schedule');
// 每隔 10 秒钟执行一次任务
const job = schedule.scheduleJob('*/10 * * * * *', function(){
console.log('每隔10秒执行一次');
});
这个例子设置了一个每隔 10 秒钟执行一次的任务。注意语法,
*/10
代表秒位,设置每10秒
执行一次
- 可以取消定时任务
javascript
const job = schedule.scheduleJob('*/5 * * * *', function(){
console.log('每5秒执行一次');
});
// 在 30 秒后取消该任务
setTimeout(() => {
job.cancel();
console.log('任务已取消');
}, 30000);
job.cancel()
取消先前设定的定时任务
node-schedule适用场景:
- 定时任务:定期进行数据清理、报告生成等任务。
- 提醒服务:比如设置定时提醒用户执行某些操作。
- 任务调度:可以用来在特定时间执行服务,比如每隔一段时间拉取数据、同步数据等。
横向扩展Python的apscheduler
- python中也有类似的执行定时任务的库,比如:APScheduler
- 以下代码,演示一下,每间隔5秒钟,执行一次任务
py
# 导入apscheduler调度器模块包
from apscheduler.schedulers.blocking import BlockingScheduler
# 导入时间模块包
from datetime import datetime
# 定义任务函数
def job():
print("任务执行了!当前时间:", datetime.now())
# 创建调度器
scheduler = BlockingScheduler()
# 添加任务,定时每隔 5 秒钟执行一次
scheduler.add_job(job, 'interval', seconds=5)
# 启动调度器
scheduler.start()
需求都是相通的...别的语言,也有类似的包,不赘述
功能实现
准备包和引入使用
- 本案例中使用的
"node-schedule": "^2.1.1"
、"express": "^4.21.1",
- 使用模块化路由
js
const express = require('express');
const fs = require('fs');
const path = require('path');
const schedule = require('node-schedule');
......
module.exports = route;
写入日志
这里的日志,使用fs.appendFileSync
方法,有这个日志文件,就在文件中,追加日志文字,没有的话,就新建这个文件,并追加日志文字
js
// 日志文件路径
const LOG_FILE = path.join(__dirname, 'schedule.log');
// 写入日志的函数
function writeLog(message) {
const timestamp = new Date().toLocaleString();
const logMessage = `[${timestamp}] ${message}\n`;
try {
fs.appendFileSync(LOG_FILE, logMessage, 'utf8');
} catch (error) {
// 如果写入日志失败,仍然输出到控制台作为备用
console.error('写入日志文件失败:', error.message);
console.log(logMessage.trim());
}
}
清理某个文件夹中的所有文件
- 这里,文件夹中都是文件,不考虑文件夹嵌套文件夹情况了
- 假设,我要清理的文件夹是,C盘下的kkk文件夹
const TARGET_FOLDER = 'C:\\kkk';
js
// 清除文件夹中的所有文件
function cleanFolder(TARGET_FOLDER) {
try {
if (!fs.existsSync(TARGET_FOLDER)) {
writeLog(`文件夹 ${TARGET_FOLDER} 不存在,跳过清理`);
return;
}
// 检查文件夹是否为空
const files = fs.readdirSync(TARGET_FOLDER);
if (files.length === 0) {
writeLog(`文件夹 ${TARGET_FOLDER} 为空,跳过清理`);
return;
}
// 开始清理 - 只删除文件,不处理子文件夹
writeLog(`开始清理文件夹: ${TARGET_FOLDER}`);
let deletedCount = 0;
files.forEach(file => {
const filePath = path.join(TARGET_FOLDER, file);
const stats = fs.lstatSync(filePath);
if (stats.isFile()) {
fs.unlinkSync(filePath);
writeLog(`已删除文件: ${file}`);
deletedCount++;
} else if (stats.isDirectory()) {
writeLog(`跳过文件夹: ${file} (不处理子文件夹)`);
}
});
writeLog(`清理完成!共删除 ${deletedCount} 个文件,时间: ${new Date().toLocaleString()}`);
} catch (error) {
writeLog(`清理文件夹时出错: ${error.message}`);
}
}
// 目标文件夹路径(需要清理的文件夹)
const TARGET_FOLDER = 'C:\\kkk';
// 特定时机执行清理函数
// cleanFolder(TARGET_FOLDER)
使用schedule.scheduleJob创建定时任务
这样的话,在每天的固定时间点,12点30分30秒,就会自动执行schedule.scheduleJob
中的回调函数
js
// 设置定时任务 - 每天12点30分30秒执行清理
const scheduleRule = '30 30 12 * * *'; // 秒 分 时 日 月 星期
let scheduledJob = schedule.scheduleJob(scheduleRule, () => {
writeLog(`开始执行定时清理任务 - ${new Date().toLocaleString()}`);
cleanFolder(TARGET_FOLDER);
});
手动执行清理任务和查看清理日志文件
发个请求,自己清理
js
// 添加路由来手动触发清理
route.get('/manualClean', (req, res) => {
try {
writeLog('手动触发清理任务');
cleanFolder(TARGET_FOLDER);
res.json({
success: true,
message: '清理任务已执行',
time: new Date().toLocaleString()
});
} catch (error) {
writeLog(`清理任务执行失败: ${error.message}`);
res.status(500).json({
success: false,
message: '清理任务执行失败',
error: error.message
});
}
});
看看日志记录
js
// 添加路由来查看日志文件
route.get('/viewLog', (req, res) => {
try {
if (fs.existsSync(LOG_FILE)) {
const logContent = fs.readFileSync(LOG_FILE, 'utf8');
res.set('Content-Type', 'text/plain');
res.send(logContent);
} else {
res.json({ message: '日志文件不存在' });
}
} catch (error) {
res.status(500).json({
success: false,
message: '读取日志文件失败',
error: error.message
});
}
});
日志文件内容
js
[2025/6/22 17:24:23] 收到SIGINT信号,正在结束定时任务...
[2025/6/22 17:24:23] 定时任务已结束
[2025/6/22 17:24:28] 服务启动 - 进程ID: 26588
[2025/6/22 17:24:28] 定时清理任务已设置,将在每天 30 30 12 * * * 执行
[2025/6/22 17:24:28] 目标文件夹: C:\kkk
[2025/6/22 17:25:46] 手动触发清理任务
[2025/6/22 17:25:46] 开始清理文件夹: C:\kkk
[2025/6/22 17:25:46] 已删除文件: txt1.txt
[2025/6/22 17:25:46] 已删除文件: txt2.txt
[2025/6/22 17:25:46] 已删除文件: txt3.txt
[2025/6/22 17:25:46] 清理完成!共删除 3 个文件,时间: 2025/6/22 17:25:46
[2025/6/22 17:25:53] 收到SIGINT信号,正在结束定时任务...
[2025/6/22 17:25:53] 定时任务已结束
手动停止定时任务&手动启动定时任务
发请求,停止定时任务
js
// 添加路由来停止定时任务
route.get('/manualStopSchedule', (req, res) => {
try {
if (scheduledJob) {
scheduledJob.cancel();
scheduledJob = null;
writeLog('定时清理任务已停止');
res.json({
success: true,
message: '定时清理任务已停止',
time: new Date().toLocaleString()
});
} else {
res.json({
success: false,
message: '定时清理任务未在运行'
});
}
} catch (error) {
writeLog(`停止定时任务失败: ${error.message}`);
res.status(500).json({
success: false,
message: '停止定时任务失败',
error: error.message
});
}
});
发请求启动定时任务
js
// 添加路由来重新启动定时任务
route.get('/manualStartSchedule', (req, res) => {
try {
if (scheduledJob) {
res.json({
success: false,
message: '定时清理任务已在运行中'
});
} else {
scheduledJob = schedule.scheduleJob(scheduleRule, () => {
writeLog(`开始执行定时清理任务 - ${new Date().toLocaleString()}`);
cleanFolder(TARGET_FOLDER);
});
writeLog('定时清理任务已重新启动');
res.json({
success: true,
message: '定时清理任务已重新启动',
nextInvocation: scheduledJob.nextInvocation(),
time: new Date().toLocaleString()
});
}
} catch (error) {
writeLog(`启动定时任务失败: ${error.message}`);
res.status(500).json({
success: false,
message: '启动定时任务失败',
error: error.message
});
}
});
监控项目进程结束信号,从而取消任务
js
/**
* Ctrl+C停止程序服务时候,会触发SIGINT信号,从而结束定时任务
* */
process.on('SIGINT', () => {
writeLog('收到SIGINT信号,正在结束定时任务...');
if (scheduledJob) {
scheduledJob.cancel();
writeLog('定时任务已结束');
}
process.exit(0);
});
/**
* 1. 使用 kill 命令停止进程
* 2. 系统重启或关机
* 3. Docker 容器停止
* 4. PM2 等进程管理器重启服务
* 等情况,都会触发SIGTERM信号,从而结束定时任务
* */
process.on('SIGTERM', () => {
writeLog('收到SIGTERM信号,正在结束定时任务...');
if (scheduledJob) {
scheduledJob.cancel();
writeLog('定时任务已结束');
}
process.exit(0);
});
完整代码
js
const express = require('express');
const fs = require('fs');
const path = require('path');
const schedule = require('node-schedule');
const route = express.Router();
// 日志文件路径
const LOG_FILE = path.join(__dirname, 'schedule.log');
// 写入日志的函数
function writeLog(message) {
const timestamp = new Date().toLocaleString();
const logMessage = `[${timestamp}] ${message}\n`;
try {
fs.appendFileSync(LOG_FILE, logMessage, 'utf8');
} catch (error) {
// 如果写入日志失败,仍然输出到控制台作为备用
console.error('写入日志文件失败:', error.message);
console.log(logMessage.trim());
}
}
// 清除文件夹中的所有文件
function cleanFolder(TARGET_FOLDER) {
try {
if (!fs.existsSync(TARGET_FOLDER)) {
writeLog(`文件夹 ${TARGET_FOLDER} 不存在,跳过清理`);
return;
}
// 检查文件夹是否为空
const files = fs.readdirSync(TARGET_FOLDER);
if (files.length === 0) {
writeLog(`文件夹 ${TARGET_FOLDER} 为空,跳过清理`);
return;
}
// 开始清理 - 只删除文件,不处理子文件夹
writeLog(`开始清理文件夹: ${TARGET_FOLDER}`);
let deletedCount = 0;
files.forEach(file => {
const filePath = path.join(TARGET_FOLDER, file);
const stats = fs.lstatSync(filePath);
if (stats.isFile()) {
fs.unlinkSync(filePath);
writeLog(`已删除文件: ${file}`);
deletedCount++;
} else if (stats.isDirectory()) {
writeLog(`跳过文件夹: ${file} (不处理子文件夹)`);
}
});
writeLog(`清理完成!共删除 ${deletedCount} 个文件,时间: ${new Date().toLocaleString()}`);
} catch (error) {
writeLog(`清理文件夹时出错: ${error.message}`);
}
}
// 目标文件夹路径(需要清理的文件夹)
const TARGET_FOLDER = 'C:\\kkk';
// 设置定时任务 - 每天12点30分30秒执行清理
const scheduleRule = '30 30 12 * * *'; // 秒 分 时 日 月 星期
let scheduledJob = schedule.scheduleJob(scheduleRule, () => {
writeLog(`开始执行定时清理任务 - ${new Date().toLocaleString()}`);
cleanFolder(TARGET_FOLDER);
});
// 添加路由来手动触发清理
route.get('/manualClean', (req, res) => {
try {
writeLog('手动触发清理任务');
cleanFolder(TARGET_FOLDER);
res.json({
success: true,
message: '清理任务已执行',
time: new Date().toLocaleString()
});
} catch (error) {
writeLog(`清理任务执行失败: ${error.message}`);
res.status(500).json({
success: false,
message: '清理任务执行失败',
error: error.message
});
}
});
// 添加路由来查看定时任务状态
route.get('/scheduleStatus', (req, res) => {
res.json({
scheduled: scheduledJob ? true : false,
nextInvocation: scheduledJob ? scheduledJob.nextInvocation() : null,
rule: scheduleRule,
targetFolder: TARGET_FOLDER
});
});
// 添加路由来停止定时任务
route.get('/manualStopSchedule', (req, res) => {
try {
if (scheduledJob) {
scheduledJob.cancel();
scheduledJob = null;
writeLog('定时清理任务已停止');
res.json({
success: true,
message: '定时清理任务已停止',
time: new Date().toLocaleString()
});
} else {
res.json({
success: false,
message: '定时清理任务未在运行'
});
}
} catch (error) {
writeLog(`停止定时任务失败: ${error.message}`);
res.status(500).json({
success: false,
message: '停止定时任务失败',
error: error.message
});
}
});
// 添加路由来重新启动定时任务
route.get('/manualStartSchedule', (req, res) => {
try {
if (scheduledJob) {
res.json({
success: false,
message: '定时清理任务已在运行中'
});
} else {
scheduledJob = schedule.scheduleJob(scheduleRule, () => {
writeLog(`开始执行定时清理任务 - ${new Date().toLocaleString()}`);
cleanFolder(TARGET_FOLDER);
});
writeLog('定时清理任务已重新启动');
res.json({
success: true,
message: '定时清理任务已重新启动',
nextInvocation: scheduledJob.nextInvocation(),
time: new Date().toLocaleString()
});
}
} catch (error) {
writeLog(`启动定时任务失败: ${error.message}`);
res.status(500).json({
success: false,
message: '启动定时任务失败',
error: error.message
});
}
});
// 添加路由来查看日志文件
route.get('/viewLog', (req, res) => {
try {
if (fs.existsSync(LOG_FILE)) {
const logContent = fs.readFileSync(LOG_FILE, 'utf8');
res.set('Content-Type', 'text/plain');
res.send(logContent);
} else {
res.json({ message: '日志文件不存在' });
}
} catch (error) {
res.status(500).json({
success: false,
message: '读取日志文件失败',
error: error.message
});
}
});
// 服务启动时写入日志
writeLog(`服务启动 - 进程ID: ${process.pid}`);
writeLog(`定时清理任务已设置,将在每天 ${scheduleRule} 执行`);
writeLog(`目标文件夹: ${TARGET_FOLDER}`);
/**
* Ctrl+C停止程序服务时候,会触发SIGINT信号,从而结束定时任务
* */
process.on('SIGINT', () => {
writeLog('收到SIGINT信号,正在结束定时任务...');
if (scheduledJob) {
scheduledJob.cancel();
writeLog('定时任务已结束');
}
process.exit(0);
});
/**
* 1. 使用 kill 命令停止进程
* 2. 系统重启或关机
* 3. Docker 容器停止
* 4. PM2 等进程管理器重启服务
* 等情况,都会触发SIGTERM信号,从而结束定时任务
* */
process.on('SIGTERM', () => {
writeLog('收到SIGTERM信号,正在结束定时任务...');
if (scheduledJob) {
scheduledJob.cancel();
writeLog('定时任务已结束');
}
process.exit(0);
});
module.exports = route;
A good memory is better than a bad pen. Record it down...