Stories

Detail Return Return

express使用node-schedule實現定時任務,比如定時清理文件夾中的文件寫入日誌功能 - Stories Detail

需求描述

  • 日常開發中,我們常常會要執行一些定時任務
  • 比如定時清理文件夾,定時發郵件等
  • 本文是在node的express框架中用node-schedule這個包
  • 來實現定時清理文件夾功能

node-schedule介紹

  • node-schedule 是一個用於在 Node.js 環境中調度和執行任務的庫。
  • 這個包可以設置定時任務、週期性任務以及一次性任務
  • 很靈活,很強大,精度高,也支持異步任務

幾個簡單案例

  • 每天十二點,觸發任務
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秒,執行一次任務
const schedule = require('node-schedule');

// 每隔 10 秒鐘執行一次任務
const job = schedule.scheduleJob('*/10 * * * * *', function(){
  console.log('每隔10秒執行一次');
});
這個例子設置了一個每隔 10 秒鐘執行一次的任務。注意語法,*/10 代表秒位,設置每10秒執行一次
  • 可以取消定時任務
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秒鐘,執行一次任務
# 導入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",
  • 使用模塊化路由
const express = require('express');
const fs = require('fs');
const path = require('path');
const schedule = require('node-schedule');

......

module.exports = route;

寫入日誌

這裏的日誌,使用fs.appendFileSync方法,有這個日誌文件,就在文件中,追加日誌文字,沒有的話,就新建這個文件,並追加日誌文字

// 日誌文件路徑
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';
// 清除文件夾中的所有文件
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中的回調函數

// 設置定時任務 - 每天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('/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
        });
    }
});

日誌文件內容

[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] 定時任務已結束

手動停止定時任務&手動啓動定時任務

發請求,停止定時任務

// 添加路由來停止定時任務
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
        });
    }
});

監控項目進程結束信號,從而取消任務

/**
 * 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);
});

完整代碼

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...
user avatar talkcss Avatar damonxiaozhi Avatar code500g Avatar shine_zhu Avatar runyubingxue Avatar angular4 Avatar dexteryao Avatar jueqiangderijiben_xuc2 Avatar jiangpengfei_5ecce944a3d8a Avatar weirdo_5f6c401c6cc86 Avatar tianxingshengjun Avatar
Favorites 11 users favorite the story!
Favorites

Add a new Comments

Some HTML is okay.