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)     );      }
 
  | 
 
成熟的库
参考链接