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...

相关推荐
ikoala15 分钟前
Codex 不得不装的 12 个插件,都在这了
前端·javascript·后端
我登哥MVP1 小时前
VS Code 安装 Claude Code 并接入 DeepSeek V4 Model
人工智能·python·node.js·agent·codex·deepseek·claude code
赵庆明老师1 小时前
JS检查提交的文件是否合规
开发语言·前端·javascript
颂love1 小时前
Vue的两大生态以及组件通信
前端·javascript·vue.js·typescript
光影少年1 小时前
js单线程,为什在node环境下的js可以处理高并发请求?
前端·javascript·掘金·金石计划
moMo2 小时前
# JavaScript 的“等等我”:聊聊同步与异步
javascript
Cobyte2 小时前
19.Vue Vapor 的实现原理原来这么简单
前端·javascript·vue.js
JackieDYH2 小时前
uniapp vue3 常用的生命周期和作用使用时机
javascript·vue.js·uni-app
Patrick_Wilson2 小时前
Node.js SSR 内存治理:为什么 --max-old-space-size 不等于进程内存
kubernetes·node.js·v8
hhb_6182 小时前
TypeScript泛型实战:企业级请求封装全解析
javascript·ubuntu·typescript