node.js監聽文件變化
前言
隨著前端技術的飛速發展,前端開發也從原始的刀耕火種,向著工程化效率化的方向發展。在各種開發框架之外,打包編譯等技術也是層出不窮,開發體驗也是越來越好。例如HMR,讓我們的更新可以即時可見,告別了手動F5的情況。其實現就是監聽文件變化自動調用構建過程。下面就關註下如何實現node監聽文件變化。
場景
假定要監聽index.js,每當內容更改重新編譯。
我們就用簡單的console來標識執行編譯。下面就是實現該功能。
node原生API
fs.watchFile
翻下node的文檔就會看到一個滿足我們需求的Apifs.watchFile(畢竟是文件相關的操作,很大可能就在fs模塊下面了)。
fs.watchFile(filename[, options], listener)
- filename 顯然就是文件名
options 可選 對象 包含以下兩個屬性
- persistent 文件被監聽時進程是否繼續,默認true
- interval 多長時間輪訓一次目標文件,默認5007毫秒
- listener 事件回調 包含兩個參數
- current 當前文件stat對象
- prev 之前文件stat對象
看完參數信息,不知道大家有沒有從其參數屬性中得到點什麽特別的信息。特別是interval選項和listener中的回調參數。
監控filename對應文件,每當訪問文件時會觸發回調。
這裏每當訪問文件時會觸發,實際指的是每次切換之後再次進入文件,然後保存之後,無論是否做了修改都會出發回調。
另外輪詢事件和文件對象,是不是可以猜測,其實現監聽的原理,固定時間輪詢文件狀態,然後將前後的狀態返回,將判斷交給使用者。
所以node也建議,如果要獲取文件修改,那麽需要根據stat對象的修改時間來進行對比,即比較 curr.mtime 和 prev.mtime。
這樣就有點問題,我們先看下例子,會更清晰一點。
const fs = require(‘fs‘) const filePath = ‘./index.js‘ console.log(`正在監聽 ${filePath}`); fs.watchFile(filePath, (cur, prv) => { if (filePath) { // 打印出修改時間 console.log(`cur.mtime>>${cur.mtime.toLocaleString()}`) console.log(`prv.mtime>>${prv.mtime.toLocaleString()}`) // 根據修改時間判斷做下區分,以分辨是否更改 if (cur.mtime != prv.mtime){ console.log(`${filePath}文件發生更新`) } } })
然後測試結果如下:
// 運行
node watch1.js
// 1、訪問index.js 不做修改,然後保存
// 2、切換文件,再次訪問,不做修改,只報錯
// 3、編輯內容,並保存
可以看到1、2兩步,並沒有實際修改內容,然而我們並沒有辦法區分。只要你是切換之後再保存,修改時間戳mtime就發生變化。
另外響應時間真的很慢,畢竟是輪詢。
對於這些問題,其實官網也給了一句話:
Using fs.watch() is more efficient than fs.watchFile and fs.unwatchFile. fs.watch should be used instead of fs.watchFile and fs.unwatchFile when possible.
能用fs.watch的情況就不要用watchFile了。一是效率,二是不能準確獲知修改狀態 三是只能監聽單獨文件
對於實際開發過程中,顯然我們想要關註的是源文件夾的變動。
fs.watch
首先用法如下:
fs.watch(filename[, options][, listener])
跟fs.watchFile比較類似。
- filename 顯然就是文件名
options 可選 對象或者字符串 包含以下三個屬性
- persistent 文件被監聽時進程是否繼續,默認true
- recursive 是否監控所有子目錄,默認false 即當前目錄,true為所有子目錄。
- encoding 指定傳遞給回調事件的文件名稱,默認utf8
- listener 事件回調 包含兩個參數
- eventType 事件類型,rename 或者 change
- filename 當前變更文件的文件名
options如果是字符串,指的是encoding。
監聽filename對應的文件或者文件夾(recursive參數也體現出來這一特性),返回一個fs.FSWatcher對象。
該功能的實現依賴於底層操作系統的對於文件更改的通知。 所以就存在一個問題,可能不同平臺的實現不太相同。
如下示例1:
const fs = require(‘fs‘)
const filePath = ‘./‘
console.log(`正在監聽 ${filePath}`);
fs.watch(filePath,(event,filename)=>{
if (filename){
console.log(`${filename}文件發生更新`)
}
})
一個比較明顯的優勢就體現出來了:響應比較及時,相比於輪詢,效率肯定更高。
不過這樣修改並保存的時候回發現同樣有點問題。
直接保存,顯示兩次更新
修改文件之後,同樣顯示兩次更新(mac系統上是兩次,其他系統可能有所差別)
這樣可能是於操作系統對文件修改的事件支持有關,在保存的時候出發了不止一次。
下面聚焦於回調事件的參數,event對應事件類型,是否可以判斷事件類型為change呢,才執行呢,忽略空保存。
const fs = require(‘fs‘)
const filePath = ‘./‘
console.log(`正在監聽 ${filePath}`);
fs.watch(filePath,(event,filename)=>{
console.log(`event類型${event}`)
if (filename && event == ‘change‘) {
console.log(`${filename}文件發生更新`)
}
})
不過實際上,空的保存event也是change,另外不同平臺event的實現可能也有所不同。這種方式要pass掉。
校驗變更時間
顯然從上面的例子可以看到,變更時間依然不可控。因為每次保存,node對應stat對象依然會修改。
對比文件內容
只能選擇這種方式來判斷是否是否更新。例如md5:
const fs = require(‘fs‘),
md5 = require(‘md5‘);
const filePath = ‘./‘
let preveMd5 = null
console.log(`正在監聽 ${filePath}`);
fs.watch(filePath,(event,filename)=>{
var currentMd5 = md5(fs.readFileSync(filePath + filename))
if (currentMd5 == preveMd5) {
return
}
preveMd5 = currentMd5
console.log(`${filePath}文件發生更新`)
})
先保存當前文件md5值,每次文件變化時(即保存操作響應之後),每次都獲取文件的md5然後進行對比,看是否發生變化。
不過這樣可以看到,當初次保存時,都會執行一次,因為初始值為null的緣故。這樣可以加個兼容,根據是否第一次保存來判斷好了。
優化
對於不同的操作系統,可能保存時觸發的回調不止一個(mac上到沒出現)。為了避免這種實時響應對應的頻繁觸發,可以引入debounce函數來保證性能。
const fs = require(‘fs‘),
md5 = require(‘md5‘);
let preveMd5 = null,
fsWait = false
const filePath = ‘./‘
console.log(`正在監聽 ${filePath}`);
fs.watch(filePath,(event,filename)=>{
if (filename){
if (fsWait) return;
fsWait = setTimeout(() => {
fsWait = false;
}, 100)
var currentMd5 = md5(fs.readFileSync(filePath + filename))
if (currentMd5 == preveMd5){
return
}
preveMd5 = currentMd5
console.log(`${filePath}文件發生更新`)
}
})
結束語
到這裏,node監聽文件的實現就結束了。做個學習筆記,來做個參考記錄。實現起來並不難,但是要實際應用的話需要考慮的方面就比較多了。還是推薦開源框架node-watch、chokidar等,各方面實現的都比較完善。更多請轉我的博客
參考文章
node文檔
How to Watch for Files Changes in Node.js
Nodejs Monitor File Changes
node.js監聽文件變化