express使用node-schedule实现定时任务,比如定时清理文件夹中的文件写入日志功能

需求描述

  • 日常开发中,我们常常会要执行一些定时任务
  • 比如定时清理文件夹,定时发邮件等
  • 本文是在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...

相关推荐
爱分享的程序员6 分钟前
前端面试专栏-主流框架:11. React Router路由原理与实践
前端·javascript·react.js·面试
weixin_4590743510 分钟前
在el-image组件的预览中添加打印功能(自定义功能)
前端·javascript·vue.js
海的诗篇_42 分钟前
前端开发面试题总结-vue3框架篇(二)
前端·javascript·vue.js·面试·前端框架·vue
广药门徒1 小时前
ad24智能pdf输出的装配图没有四个边角那里的圆孔
前端·javascript·pdf
粥里有勺糖2 小时前
视野修炼第124期 | 终端艺术字
前端·javascript·github
托尼沙滩裤2 小时前
【Node】最佳Node.js后端开发模板推荐
node.js
暴怒的代码2 小时前
解决Vue2官网Webpack源码泄露漏洞
前端·webpack·node.js
阿珊和她的猫3 小时前
组件之间的双向绑定:v-model
前端·javascript·vue.js·typescript
爱分享的程序员3 小时前
Node.js 实训专栏规划目录
前端·javascript·node.js
Q_Q5110082854 小时前
python的校园兼职系统
开发语言·spring boot·python·django·flask·node.js·php