node.js監聽檔案變化

瀟湘待雨發表於2019-04-17

前言

隨著前端技術的飛速發展,前端開發也從原始的刀耕火種,向著工程化效率化的方向發展。在各種開發框架之外,打包編譯等技術也是層出不窮,開發體驗也是越來越好。例如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、編輯內容,並儲存
複製程式碼

node.js監聽檔案變化

可以看到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系統上是兩次,其他系統可能有所差別) node.js監聽檔案變化 這樣可能是於作業系統對檔案修改的事件支援有關,在儲存的時候出發了不止一次。
下面聚焦於回撥事件的引數,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然後進行對比,看是否發生變化。
node.js監聽檔案變化 不過這樣可以看到,當初次儲存時,都會執行一次,因為初始值為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

相關文章