Keywords: 操作系统差异、识别用户/编辑器操作、连续触发的优化、工程级 API。
概述
NodeJS 提供了 fs.watch / fs.watchFile 两种 API:
- fs.watch: 推荐,可以监听文件夹。基于操作系统。
- fs.watchFile: 只能监听指定文件。并且通过轮询检测文件变化,不能响应实时反馈。
一个监听指定文件夹的代码如下:
1 2 3 4 5
| fs.watch(dir, { recursive: true }, (eventType, file) => { if (file && eventType === "change") { console.log(`${file} 已经改变`); } })
|
跨平台优化
对于不同系统内核,比如 maxos,fs.watch 回调函数中的第一个参数,不会监听到 rename、delete 事件。因此,这不是一个工程级别的可用 api。
文件 md5
某些开源软件,会将文件内容都清空后,再添加内容。而且保存过程中,可能会出现多个中间态。
对于文件更改的情况,检测内容的 md5 值,是个不错的方法。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| let previousMD5 = ""; fs.watch("./whatever", (type, filename) => { if (!filename) { return; } const md5 = crypto.createHash("md5"); const currentMD5 = md5 .update(fs.readFileSync(filename).toString()) .digest("hex"); if (currentMD5 === previousMD5) { return; } previousMD5 = currentMD5; console.log(`${filename} is changed`); });
|
事件频率控制
对于文件变更,不同的系统可能会触发多个不同的中间态。因此,借助 debounce 函数的思想,控制和修正回调事件的触发频率。
前面的代码修正为:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| let previousMD5 = ""; let watchWait = false; fs.watch("./whatever", (type, filename) => { if (!filename || watchWait) { return; } watchWait = setTimeout(() => { watchWait = false; }, 100); const md5 = crypto.createHash("md5"); const currentMD5 = md5 .update(fs.readFileSync(filename).toString()) .digest("hex"); if (currentMD5 === previousMD5) { return; } previousMD5 = currentMD5; console.log(`${filename} is changed`); })
|
文件信息
对于常见的库来说,除了不信任原生 API、使用上述技巧外,很重要的是,都根据 fs.Stats 类的信息,自定义逻辑来判断文件状态,以此保证不同平台兼容性。
下面是在 Node10 中,打印的文件状态信息:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| Stats { dev: 16777222, mode: 33188, nlink: 1, uid: 501, gid: 20, rdev: 0, blksize: 4096, ino: 6493141, size: 7, blocks: 8, atimeMs: 1567516873292.676, mtimeMs: 1567516873293.3867, ctimeMs: 1567516873293.3867, birthtimeMs: 1566547653640.1763, atime: 2019-09-03T13:21:13.293Z, mtime: 2019-09-03T13:21:13.293Z, ctime: 2019-09-03T13:21:13.293Z, birthtime: 2019-08-23T08:07:33.640Z }
|
通过文件信息的思路,就是在fs.stat()
的回调函数中,进行逻辑处理:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| function awaitWriteFinish() { fs.stat( fullPath, function(err, curStat) { if (prevStat && curStat.size != prevStat.size) { this._pendingWrites[path].lastChange = now; } if (now - this._pendingWrites[path].lastChange >= threshold) { delete this._pendingWrites[path]; awfEmit(null, curStat); } else { timeoutHandler = setTimeout( awaitWriteFinish.bind(this, curStat), this.options.awaitWriteFinish.pollInterval ); } }.bind(this) ); }
|
成熟的库
参考链接