node.js監(jiān)聽(tīng)文件變化的實(shí)現(xiàn)方法
前言
隨著前端技術(shù)的飛速發(fā)展,前端開(kāi)發(fā)也從原始的刀耕火種,向著工程化效率化的方向發(fā)展。在各種開(kāi)發(fā)框架之外,打包編譯等技術(shù)也是層出不窮,開(kāi)發(fā)體驗(yàn)也是越來(lái)越好。例如HMR,讓我們的更新可以即時(shí)可見(jiàn),告別了手動(dòng)F5的情況。其實(shí)現(xiàn)就是監(jiān)聽(tīng)文件變化自動(dòng)調(diào)用構(gòu)建過(guò)程。下面就關(guān)注下如何實(shí)現(xiàn)node監(jiān)聽(tīng)文件變化。
場(chǎng)景
假定要監(jiān)聽(tīng)index.js,每當(dāng)內(nèi)容更改重新編譯。
我們就用簡(jiǎn)單的console來(lái)標(biāo)識(shí)執(zhí)行編譯。下面就是實(shí)現(xiàn)該功能。
node原生API
fs.watchFile
翻下node的文檔就會(huì)看到一個(gè)滿足我們需求的Apifs.watchFile(畢竟是文件相關(guān)的操作,很大可能就在fs模塊下面了)。
fs.watchFile(filename[, options], listener)
- filename 顯然就是文件名
- options 可選 對(duì)象 包含以下兩個(gè)屬性
- persistent 文件被監(jiān)聽(tīng)時(shí)進(jìn)程是否繼續(xù),默認(rèn)true
- interval 多長(zhǎng)時(shí)間輪訓(xùn)一次目標(biāo)文件,默認(rèn)5007毫秒
- listener 事件回調(diào) 包含兩個(gè)參數(shù)
- current 當(dāng)前文件stat對(duì)象
- prev 之前文件stat對(duì)象
看完參數(shù)信息,不知道大家有沒(méi)有從其參數(shù)屬性中得到點(diǎn)什么特別的信息。特別是interval選項(xiàng)和listener中的回調(diào)參數(shù)。
監(jiān)控filename對(duì)應(yīng)文件,每當(dāng)訪問(wèn)文件時(shí)會(huì)觸發(fā)回調(diào)。
這里每當(dāng)訪問(wèn)文件時(shí)會(huì)觸發(fā),實(shí)際指的是每次切換之后再次進(jìn)入文件,然后保存之后,無(wú)論是否做了修改都會(huì)出發(fā)回調(diào)。
另外輪詢事件和文件對(duì)象,是不是可以猜測(cè),其實(shí)現(xiàn)監(jiān)聽(tīng)的原理,固定時(shí)間輪詢文件狀態(tài),然后將前后的狀態(tài)返回,將判斷交給使用者。
所以node也建議,如果要獲取文件修改,那么需要根據(jù)stat對(duì)象的修改時(shí)間來(lái)進(jìn)行對(duì)比,即比較 curr.mtime 和 prev.mtime。
這樣就有點(diǎn)問(wèn)題,我們先看下例子,會(huì)更清晰一點(diǎn)。
const fs = require('fs')
const filePath = './index.js'
console.log(`正在監(jiān)聽(tīng) ${filePath}`);
fs.watchFile(filePath, (cur, prv) => {
if (filePath) {
// 打印出修改時(shí)間
console.log(`cur.mtime>>${cur.mtime.toLocaleString()}`)
console.log(`prv.mtime>>${prv.mtime.toLocaleString()}`)
// 根據(jù)修改時(shí)間判斷做下區(qū)分,以分辨是否更改
if (cur.mtime != prv.mtime){
console.log(`${filePath}文件發(fā)生更新`)
}
}
})
然后測(cè)試結(jié)果如下:
// 運(yùn)行
node watch1.js
// 1、訪問(wèn)index.js 不做修改,然后保存
// 2、切換文件,再次訪問(wèn),不做修改,只報(bào)錯(cuò)
// 3、編輯內(nèi)容,并保存

可以看到1、2兩步,并沒(méi)有實(shí)際修改內(nèi)容,然而我們并沒(méi)有辦法區(qū)分。只要你是切換之后再保存,修改時(shí)間戳mtime就發(fā)生變化。
另外響應(yīng)時(shí)間真的很慢,畢竟是輪詢。
對(duì)于這些問(wèn)題,其實(shí)官網(wǎng)也給了一句話:
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了。一是效率,二是不能準(zhǔn)確獲知修改狀態(tài) 三是只能監(jiān)聽(tīng)單獨(dú)文件
對(duì)于實(shí)際開(kāi)發(fā)過(guò)程中,顯然我們想要關(guān)注的是源文件夾的變動(dòng)。
fs.watch
首先用法如下:
fs.watch(filename[, options][, listener])
跟fs.watchFile比較類(lèi)似。
- filename 顯然就是文件名
- options 可選 對(duì)象或者字符串 包含以下三個(gè)屬性
- persistent 文件被監(jiān)聽(tīng)時(shí)進(jìn)程是否繼續(xù),默認(rèn)true
- recursive 是否監(jiān)控所有子目錄,默認(rèn)false 即當(dāng)前目錄,true為所有子目錄。
- encoding 指定傳遞給回調(diào)事件的文件名稱,默認(rèn)utf8
- listener 事件回調(diào) 包含兩個(gè)參數(shù)
- eventType 事件類(lèi)型,rename 或者 change
- filename 當(dāng)前變更文件的文件名
options如果是字符串,指的是encoding。
監(jiān)聽(tīng)filename對(duì)應(yīng)的文件或者文件夾(recursive參數(shù)也體現(xiàn)出來(lái)這一特性),返回一個(gè)fs.FSWatcher對(duì)象。
該功能的實(shí)現(xiàn)依賴于底層操作系統(tǒng)的對(duì)于文件更改的通知。 所以就存在一個(gè)問(wèn)題,可能不同平臺(tái)的實(shí)現(xiàn)不太相同。
如下示例1:
const fs = require('fs')
const filePath = './'
console.log(`正在監(jiān)聽(tīng) ${filePath}`);
fs.watch(filePath,(event,filename)=>{
if (filename){
console.log(`${filename}文件發(fā)生更新`)
}
})
一個(gè)比較明顯的優(yōu)勢(shì)就體現(xiàn)出來(lái)了:響應(yīng)比較及時(shí),相比于輪詢,效率肯定更高。
不過(guò)這樣修改并保存的時(shí)候回發(fā)現(xiàn)同樣有點(diǎn)問(wèn)題。
直接保存,顯示兩次更新
修改文件之后,同樣顯示兩次更新(mac系統(tǒng)上是兩次,其他系統(tǒng)可能有所差別)

這樣可能是于操作系統(tǒng)對(duì)文件修改的事件支持有關(guān),在保存的時(shí)候出發(fā)了不止一次。
下面聚焦于回調(diào)事件的參數(shù),event對(duì)應(yīng)事件類(lèi)型,是否可以判斷事件類(lèi)型為change呢,才執(zhí)行呢,忽略空保存。
const fs = require('fs')
const filePath = './'
console.log(`正在監(jiān)聽(tīng) ${filePath}`);
fs.watch(filePath,(event,filename)=>{
console.log(`event類(lèi)型${event}`)
if (filename && event == 'change') {
console.log(`${filename}文件發(fā)生更新`)
}
})
不過(guò)實(shí)際上,空的保存event也是change,另外不同平臺(tái)event的實(shí)現(xiàn)可能也有所不同。這種方式要pass掉。
校驗(yàn)變更時(shí)間
顯然從上面的例子可以看到,變更時(shí)間依然不可控。因?yàn)槊看伪4妫琻ode對(duì)應(yīng)stat對(duì)象依然會(huì)修改。
對(duì)比文件內(nèi)容
只能選擇這種方式來(lái)判斷是否是否更新。例如md5:
const fs = require('fs'),
md5 = require('md5');
const filePath = './'
let preveMd5 = null
console.log(`正在監(jiān)聽(tīng) ${filePath}`);
fs.watch(filePath,(event,filename)=>{
var currentMd5 = md5(fs.readFileSync(filePath + filename))
if (currentMd5 == preveMd5) {
return
}
preveMd5 = currentMd5
console.log(`${filePath}文件發(fā)生更新`)
})
先保存當(dāng)前文件md5值,每次文件變化時(shí)(即保存操作響應(yīng)之后),每次都獲取文件的md5然后進(jìn)行對(duì)比,看是否發(fā)生變化。

不過(guò)這樣可以看到,當(dāng)初次保存時(shí),都會(huì)執(zhí)行一次,因?yàn)槌跏贾禐閚ull的緣故。這樣可以加個(gè)兼容,根據(jù)是否第一次保存來(lái)判斷好了。
優(yōu)化
對(duì)于不同的操作系統(tǒng),可能保存時(shí)觸發(fā)的回調(diào)不止一個(gè)(mac上到?jīng)]出現(xiàn))。為了避免這種實(shí)時(shí)響應(yīng)對(duì)應(yīng)的頻繁觸發(fā),可以引入debounce函數(shù)來(lái)保證性能。
const fs = require('fs'),
md5 = require('md5');
let preveMd5 = null,
fsWait = false
const filePath = './'
console.log(`正在監(jiān)聽(tīng) ${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}文件發(fā)生更新`)
}
})
結(jié)束語(yǔ)
到這里,node監(jiān)聽(tīng)文件的實(shí)現(xiàn)就結(jié)束了。做個(gè)學(xué)習(xí)筆記,來(lái)做個(gè)參考記錄。實(shí)現(xiàn)起來(lái)并不難,但是要實(shí)際應(yīng)用的話需要考慮的方面就比較多了。還是推薦開(kāi)源框架node-watch、chokidar等,各方面實(shí)現(xiàn)的都比較完善。
好了,以上就是這篇文章的全部?jī)?nèi)容了,希望本文的內(nèi)容對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,謝謝大家對(duì)腳本之家的支持。
參考文章
相關(guān)文章
Nodejs 獲取時(shí)間加手機(jī)標(biāo)識(shí)的32位標(biāo)識(shí)實(shí)現(xiàn)代碼
本文給大家分享nodejs獲取時(shí)間加手機(jī)標(biāo)識(shí)的32位標(biāo)識(shí)實(shí)現(xiàn)代碼,非常不錯(cuò),具有參考借鑒價(jià)值,需要的朋友參考下2017-03-03
node基于puppeteer模擬登錄抓取頁(yè)面的實(shí)現(xiàn)
本篇文章主要介紹了node基于puppeteer模擬登錄抓取頁(yè)面的實(shí)現(xiàn),小編覺(jué)得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧2018-05-05
NodeJS通過(guò)魔術(shù)封包喚醒局域網(wǎng)計(jì)算機(jī)實(shí)例
這篇文章主要為大家介紹了NodeJS通過(guò)魔術(shù)封包喚醒局域網(wǎng)計(jì)算機(jī)代碼實(shí)例,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-06-06
Thinkjs3新手入門(mén)之如何使用靜態(tài)資源目錄
最近在學(xué)習(xí)thinkjs3,發(fā)現(xiàn)有些地方還是有必要整理下的,下面這篇文章主要給大家介紹了關(guān)于Thinkjs3新手入門(mén)之如何使用靜態(tài)資源目錄的相關(guān)資料,文中通過(guò)示例代碼介紹的非常詳細(xì),需要的朋友可以參考下。2017-12-12

