需求描述
- 日常開發中,我們常常會要執行一些定時任務
- 比如定時清理文件夾,定時發郵件等
- 本文是在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...