1简介本期精读文章为:HowtoWatchforFilesChangesinNode.js,讨论如何监控文件变化。如果你想使用现成的库,建议使用chokidar或node-watch。如果您想了解实现原理,请继续阅读。2OverviewUsingfs.watchfile使用fs内置函数watchfile似乎可以解决问题:fs.watchFile(dir,(curr,prev)=>{});但是你可能会发现这个回调的执行有一定的延迟,因为watchfile是通过轮询和检测文件变化不能提供实时反馈的,只能监控一个文件,存在效率问题.使用fs.watchfs的另一个内置函数watch是更好的选择:fs.watch(dir,(event,filename)=>{});watch通过操作系统提供的文件变化通知机制,在Linux操作系统中使用inotify,在macOS系统中使用FSEvents,在windows系统中使用ReadDirectoryChangesW,可以用来监听目录变化。在监控文件夹的场景下,比创建N个fs.watchfiles效率高很多。$nodefile-watcher.js[2018-05-21T00:55:52.588Z]监视./button-presses.log[2018-05-21T00:56:00.773Z]button-presses.log文件上的文件更改[2018-05-21T00:56:00.793Z]button-presses.log文件已更改[2018-05-21T00:56:00.802Z]button-presses.log文件已更改[2018-05-21T00:56:00.813Z]button-presses.log文件已更改但是当我们修改一个文件时,回调执行了4次!原因是写文件时,即使只保存一次,也可能会触发多次写操作。但是我们不需要这么敏感的回调,因为一般认为一个save就是一个修改,我们并不关心这个文件在系统底层写了多少次。因此可以进一步判断触发状态是否为change:fs.watch(dir,(event,filename)=>{if(filename&&event==="change"){console.log(`${filename}文件已更改`);}});这样可以在一定程度上解决问题,但是笔者发现Raspbian系统不支持重命名事件。如果归类为变化,这样的判断就没有意义了。作者想表达的是,在不同的平台下,fs.watch的规则可能会有所不同。原因是fs.watch使用的是各个平台提供的API,无法保证这些API实现规则的统一性。优化方案一:基于fs.watch比较文件修改时间,增加修改时间判断:letpreviousMTime=newDate(0);fs.watch(dir,(event,filename)=>{if(filename){conststats=fs.statSync(filename);if(stats.mtime.valueOf()===previousMTime.valueOf()){返回;}previousMTime=stats.mtime;console.log(`${filename}fileChanged`);}});日志从4变成了3,但是问题依然存在。我们认为文件内容的改变算作修改,但是操作系统考虑的因素更多,所以我们尝试比较文件内容是否发生了改变。作者补充:其他一些开源编辑器可能会在写入前清空文件,这也会影响触发回调的次数。优化方案二:只有在文件内容发生变化时,验证文件的md5是否发生变化。它被认为是一种变化。这总是可能的:letmd5Previous=null;fs.watch(dir,(event,filename)=>{if(filename){constmd5Current=md5(fs.readFileSync(buttonPressesLogFile));if(md5Current===md5Previous){return;}md5Previous=md5Current;控制台。log(`${filename}fileChanged`);}});log终于从3变成2了,怎么又多了一个?可能的原因是在文件保存过程中,系统可能会触发多个回调事件,可能会有一个中间状态。优化方案三:加入延迟机制我们尝试将判断延迟100毫秒,这样可能会避免中间状态:letfsWait=false;fs.watch(dir,(event,filename)=>{if(filename){if(fsWait)return;fsWait=setTimeout(()=>{fsWait=false;},100);console.log(`${filename}fileChanged`);}});现在日志变成了一个。许多npm包使用debounce函数来控制触发频率,然后修正触发频率。而我们需要结合md5和延迟机制来得到相对准确的结果:letmd5Previous=null;让fsWait=false;fs.watch(dir,(event,filename)=>{if(filename){if(fsWait)return;fsWait=setTimeout(()=>{fsWait=false;},100);constmd5Current=md5(fs.readFileSync(dir));if(md5Current===md5Previous){return;}md5Previous=md5Current;console.log(`${filename}fileChanged`);}});3精读的作者讨论了一些基本的实现文件夹监控的方法。可以看出使用各平台原生API的fs.watch并没有Spectrum那么靠谱,但是这是我们监控文件的唯一方式,所以我们需要在它的基础上做一系列的优化。在实际场景中,还需要考虑文件夹和文件的区分、软链接、读写权限等。另外,生产环境使用的库,基本上都是用50到100毫秒来解决重复触发的问题。所以不管是chokidar还是node-watch,都大量使用了文中提到的技术,加上边界条件、软连接、权限等的处理,把所有可能的情况都考虑进去,从而提供更多准确的回调。比如判断文件写操作是否完成,也需要轮询:functionawaitWriteFinish(){//...省略fs.stat(fullPath,function(err,curStat){//...省略if(prevStat&&curStat.size!=prevStat.size){this._pendingWrites[path].lastChange=now;}if(now-this._pendingWrites[path].lastChange>=threshold){deletethis._pendingWrites[path];awfEmit(null,curStat);}else{timeoutHandler=setTimeout(awaitWriteFinish.bind(this,curStat),this.options.awaitWriteFinish.pollInterval);}}.bind(this));//...省略}可见,第三方npm库采用了不信任操作系统回调的方式,完全重写了基于文件信息的判断逻辑。可见,如果相信操作系统的回调,所有操作系统之间的差异是无法抹平的。只有统一重写“写入”、“删除”、“修改”文件的逻辑,才能保证跨平台的兼容性。4总结使用nodejs监控文件夹变化很容易,但是很难提供准确的回调。主要难点在于两点:磨平操作系统之间的差异,这需要在结合fs.watch和延迟机制的同时增加一些额外的验证机制。区分操作系统的期望和用户的期望。例如,编辑器的额外操作和操作系统的多次读写应该被忽略。用户的期望不会那么频繁,极短时间内的连续触发会被忽略。此外,还有兼容性、权限、软连接等其他因素需要考虑。fs.watch并不是开箱即用的工程级API。5更多讨论讨论地址为:精读《如何利用 Nodejs 监听文件夹》·第87期·dt-fe/weekly想参与讨论的请点这里,每周都有新话题,周末或周一发布。
