很多的nodejs工具都有修改文件后自动重启服务的功能,如vue-cli,pm2,supervisor,本文就想造一个这样的轮子看看
分析
这个功能主要遇到的问题是:
- 如何监控文件修改
- 怎么重新启动服务
如果上面的问题能解决,我们将直接使用如下代码解决问题
async function watchFile(dir, callback){};
async function restart(){};
async function run(dir) {
await watchFile(dir, restart);
}
先解决第一个问题
监控文件修改
第一种办法就是对比文件修改时间,定时扫描监控目录树
首先先写一个函数,将nodejs的非promise方法可以转换为promise方法,稍后就可以直接使用async,await来简化代码和增加可读性
// 将error first风格的函数转换为promise风格的函数
function toPromise(fn) {
return function() {
let args = Array.prototype.slice.apply(arguments);
return new Promise(((resolve, reject) => {
args.push((err, data) => {
return err?reject(err):resolve(data);
});
fn.apply(null, args);
}));
}
}
接下来需要建立一个对象来记录所有文件的修改时间,这里直接使用一个对象来记录,如: {'a/b.js': 10101010110}, key为文件路径,value为修改时间
现在来解决获取文件修改时间和遍历子目录的功能, 这里有两个选择,可以选择递归或者使用循环
先用递归实现一个吧
const fs = require('fs');
const path = require('path');
const stat = toPromise(fs.stat);
const readdir = toPromise(fs.readdir);
async function getStatusWithRecursive(dir) {
let status = {};
let file = path.resolve(dir);
let s = await stat(file);
if (!s.isDirectory()) {
status[file] = s.mtime.getTime();
} else {
let dirs = await readdir(file);
for (let d of dirs) {
let subStatus = await getStatusWithRecursive(path.join(file, d));
for (let k in subStatus) {
if (subStatus.hasOwnProperty(k)) {
status[k] = subStatus[k];
}
}
}
}
return status;
}
递归程序可能由于目录层级很深从而导致栈溢出,下面使用循环方式的一种实现
const stat = toPromise(fs.stat);
const readdir = toPromise(fs.readdir);
const path = require('path');
async function getStatusWithLoop(dir) {
let status = {};
let queue = [];
queue.push(path.resolve(dir));
while (queue.length > 0) {
let file = queue.shift();
let s = await stat(file);
if (!s.isDirectory()) {
status[file] = s.mtime.getTime();
} else {
let dirs = await readdir(file);
queue.push(...dirs.map(f => path.join(file, f)));
}
}
return status;
}
有了获取整个目录树状态的方法,现在需要一个能对比状态是否有变化的方法
async function isChange(oldStatus, newStatus) {
// 有文件删除或者新增
let oldKeys = Object.keys(oldStatus);
let newKeys = Object.keys(newStatus);
if (oldKeys.length !== newKeys.length) {
return true;
}
// 老状态中存在新状态中不存在的key (有新文件删除)
if (oldKeys.some(k => !newStatus.hasOwnProperty(k))) {
return true;
}
// 新状态中存在老状态中不存在的key (有新文件创建)
if (newKeys.some(k => !oldStatus.hasOwnProperty(k))) {
return true;
}
// 对比文件修改时间
return oldKeys.some(k => oldStatus[k] !== newStatus[k])
}
完成watchFile方法
async function watchFile(dir, callback) {
let status = await getStatusWithLoop(dir);
setInterval(async () => {
let newStatus = await getStatusWithLoop(dir);
let change = await isChange(status, newStatus);
if (change) {
callback();
}
status = newStatus;
}, 1000);
}
更好的方式实现监控文件修改
写了这么一大堆代码实现文件修改监控, 但是有过linux使用经验的话,可能听过inotify这个东西,在nodejs里也有对应的绑定,就是fs.watch, 现在使用fs模块的代码重新实现watchFile方法
const fs = require('fs');
function watchFile(dir, callback) {
fs.watch(dir, {recursive: true}, callback);
}
现在已经解决了第一个问题,可以着手解决第二个问题了
如何重新启动服务
由于node启动程序很快,我们不考虑热加载,直接使用多个进程的方式来实现,思路简单来说就是,如果文件有修改,那么就退出老的进程,然后使用新的代码创建一个新的进程
使用child_process模块实现
const child_process = require('child_process');
let pr;
function start() {
pr = child_process.exec('node index.js');
pr.on('exit', () => {
setTimeout(start, 1000);
});
console.log('restart program.');
}
watchFile('.', async () => {
if (pr) {
pr.kill();
pr = null;
}
});
start();
使用上面方式实现需要准确找到node可执行文件的位置,还有index.js所在位置, 可用性不是很好,更好的方式是使用cluster模块实现
使用cluster模块实现, 在master进程中实现文件监听,在worker进程中启动具体需要重启的程序
const cluster = require('cluster');
// 这里引入主程序代码
function serve() {
require('./index.js');
}
if (cluster.isMaster) {
let pr;
function fork() {
pr = cluster.fork();
pr.on('exit', () => {
console.log('process restart.');
// 如果子进程退出,一秒后重启(防止因为修改代码导致启动不了的时候重试速度过快)
setTimeout(fork, 1000);
})
}
watchFile('.', () => {
if (pr) {
pr.kill();
}
});
fork();
} else {
serve();
}
完整代码
const fs = require('fs');
const cluster = require('cluster');
async function watchFile(dir, callback) {
fs.watch(dir, {recursive: true}, callback)
}
function serve() {
require('./index.js');
}
if (cluster.isMaster) {
let pr;
function fork() {
pr = cluster.fork();
pr.on('exit', () => {
console.log('process restart.')
setTimeout(fork, 1000);
})
}
// 监控文件修改,如果修改了,停止子进程, (子进程有监控退出事件,会自动启动新的进程)
watchFile('.', () => {
if (pr) {
pr.kill();
}
});
// 启动子进程
fork();
} else {
serve();
}