本期精讀的文章是:How to Watch for Files Changes in Node.js,探討如何監聽文件的變化。node
若是想使用現成的庫,推薦 chokidar 或 node-watch,若是想了解實現原理,請往下閱讀。git
使用 fs
內置函數 watchfile
彷佛能夠解決問題:github
fs.watchFile(dir, (curr, prev) => {});
但你可能會發現這個回調執行有必定延遲,由於 watchfile
是經過輪詢檢測文件變化的,它並不能實時做出反饋,並且只能監聽一個文件,存在效率問題。typescript
使用 fs
的另外一個內置函數 watch
是更好的選擇:npm
fs.watch(dir, (event, filename) => {});
watch
經過操做系統提供的文件更改通知機制,在 Linux 操做系統使用 inotify,在 macOS 系統使用 FSEvents,在 windows 系統使用 ReadDirectoryChangesW,並且能夠用來監聽目錄的變化,在監聽文件夾的場景中,比建立 N 個 fs.watchfile
效率高出不少。windows
$ node file-watcher.js [2018-05-21T00:55:52.588Z] Watching for file changes on ./button-presses.log [2018-05-21T00:56:00.773Z] button-presses.log file Changed [2018-05-21T00:56:00.793Z] button-presses.log file Changed [2018-05-21T00:56:00.802Z] button-presses.log file Changed [2018-05-21T00:56:00.813Z] button-presses.log file Changed
但當咱們修改一個文件時,回調卻執行了 4 次!緣由是文件被寫入時,可能觸發屢次寫操做,即便只保存了一次。但咱們不須要這麼敏感的回調,由於一般認爲一次保存就是一次修改,系統底層寫了幾回文件咱們並不關心。api
於是能夠進一步判斷是否觸發狀態是 change
:bash
fs.watch(dir, (event, filename) => { if (filename && event === "change") { console.log(`${filename} file Changed`); } });
這樣作能夠必定程度解決問題,但做者發現 Raspbian 系統不支持 rename
事件,若是歸類爲 change
,會致使這樣的判斷毫無心義。編輯器
做者要表達的意思是,在不一樣平臺下,fs.watch
的規則可能會不一樣,緣由是fs.watch
分別使用了各平臺提供的 api,因此沒法保證這些 api 實現規則的統一性。
基於 fs.watch
,增長了對修改時間的判斷:函數
let previousMTime = new Date(0); fs.watch(dir, (event, filename) => { if (filename) { const stats = fs.statSync(filename); if (stats.mtime.valueOf() === previousMTime.valueOf()) { return; } previousMTime = stats.mtime; console.log(`${filename} file Changed`); } });
log 由 4 個變成了 3 個,但依然存在問題。咱們認爲文件內容變化纔算有修改,但操做系統考慮的因素更多,因此咱們再嘗試對比文件內容是否變化。
筆者補充:另一些開源編輯器可能先清空文件再寫入,也會影響到觸發回調的次數。
只有文件內容變化了,才認爲觸發了改動,這下總能夠了吧:
let md5Previous = null; fs.watch(dir, (event, filename) => { if (filename) { const md5Current = md5(fs.readFileSync(buttonPressesLogFile)); if (md5Current === md5Previous) { return; } md5Previous = md5Current; console.log(`${filename} file Changed`); } });
log 終於由 3 個變成了 2 個,爲何多出一個?可能的緣由是,在文件保存過程當中,系統可能會觸發多個回調事件,也許存在中間態。
咱們嘗試延遲 100 毫秒進行判斷,也許能避開中間狀態:
let fsWait = false; fs.watch(dir, (event, filename) => { if (filename) { if (fsWait) return; fsWait = setTimeout(() => { fsWait = false; }, 100); console.log(`${filename} file Changed`); } });
這下 log 變成一個了。不少 npm 包在這裏使用了 debounce 函數控制觸發頻率,纔將觸發頻率修正。
並且咱們須要結合 md5 與延遲機制共同做用,才能獲得相對精準的結果:
let md5Previous = null; let fsWait = false; fs.watch(dir, (event, filename) => { if (filename) { if (fsWait) return; fsWait = setTimeout(() => { fsWait = false; }, 100); const md5Current = md5(fs.readFileSync(dir)); if (md5Current === md5Previous) { return; } md5Previous = md5Current; console.log(`${filename} file Changed`); } });
做者討論了一些實現文件夾監聽的基本方式,能夠看出,使用了各平臺原生 API 的 fs.watch
並不那麼靠譜,但這也咱們監聽文件的惟一手段,因此須要基於它進行一系列優化。
而實際場景中,還須要考慮區分文件夾與文件、軟鏈接、讀寫權限等狀況。
另外用在生產環境的庫,也基本使用 50 到 100 毫秒解決重複觸發的問題。
因此不管 chokidar 或 node-watch,都大量使用了文中說起的技巧,再加上對邊界條件的處理,對軟鏈接、權限等狀況處理,將全部可能狀況都考慮到,才能提供較爲準確的回調。
好比判斷文件寫入操做是否完畢,也須要經過輪詢的方式:
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) ); // ...省略 }
能夠看出,第三方 npm 庫都採起不信任操做系統回調的方式,根據文件信息徹底重寫了判斷邏輯。
可見,信任操做系統的回調,就沒法抹平全部操做系統間的差別,惟有統一重寫文件的 「寫入」、「刪除」、「修改」 等邏輯,才能保證在全平臺的兼容性。
利用 nodejs 監聽文件夾變化很容易,但提供準確的回調卻很難,主要難在兩點:
fs.watch
的同時,增長一些額外校驗機制與延時機制。另外還有兼容性、權限、軟鏈接等其餘因素要考慮,fs.watch
並非一個開箱可用的工程級別 api。
討論地址是: 精讀《如何利用 Nodejs 監聽文件夾》 · Issue #87 · dt-fe/weekly
若是你想參與討論,請點擊這裏,每週都有新的主題,週末或週一發佈。