@(node,watcher)
watcher,在如今的前端領域已經數見不鮮了。目前流行的gulp流程工具提供了watcher的選項,是我們在開發過程中不需要手動進行觸發構建流程,轉而根據檔案(目錄)內容改變來觸發。
深入到watcher實現層,其實是基於node的fs.watch API,但是fs.watch有很多“不確定性”,下文會一一解答。
fs.watch
(fs.FSWatcher) fs.watch(filename[, options][, listener])
watch API很簡單,接受三個引數,並返回一個FSWatcher物件。
filename可以是檔案,也可是目錄;
options為可選物件,預設為 { persistent: true, recursive: false }
,其中persistent屬性意味著:watcher程序會一直watch該檔案(目錄),即watcher程序阻塞;recursive屬性意味著:如果監聽的是目錄,則目錄下屬的目錄和檔案也會被監聽,recursive屬性存在相容性問題,在linux系統下無效,在windows和OSX下正常。
listener為回撥函式,接受兩個引數,分別為event和filename,其中事件有兩種型別,“rename”和“change”,而filename也有相容性問題,在使用時也要注意相容性判斷。
問題
在上一節中簡單介紹了watch API,也簡單提到了一些相容性問題,在此列舉出來:
- recursive屬性在linux下失效;
- watch目錄時,回撥函式中的filename只在linux和windows下可以獲取;
- node在任何情況下都不確保filename可以獲取到
解決方案
輪訓
node提供了另一個介面,
fs.watchFile(filename[, options], listener)
返回值同為FSWatcher,引數filename可為目錄和檔案,options預設為
{ persistent: true, interval: 5007 },其中interval則為node輪訓該檔案的時間間隔,listener接受兩個引數,即類行為fs.Stat的curr和prev物件,我們可通過
curr.mtime == prev.mtime
判斷檔案是否發生改動。
不管在何種系統設計中,輪訓的方式都是相容性保底方案,只要我們的系統支援fs.watch方法,就不用採用該種方式進行相容。
那麼合適可以採用輪訓呢?我認為,大概分兩種情況:
- 需要針對檔案的元資訊判斷是否觸發事件
- 監控的檔案所在的作業系統,如果是NFS, SMB等網路檔案系統,fs.watch並不提供功能,因此只能使用輪訓方式(watch方法是基於檔案系統的特性編寫的,在linux下基於“inotify”,windows下基於“ReadDirectoryChangesW”)
手動適配
針對非網路檔案系統,watch API的相容性就在於是否遞迴watch以及OSX下filename獲取的問題,因此我們可以通過編碼方式解決:
- 採用預設的options配置,即
{ persistent: true, recursive: false }
,通過walker便利目錄,針對單個檔案作watcher - 針對單個檔案做watch,OSX可以獲取到filename
通過簡單的處理,一個簡易的watcher就實現了,配合著EventEmit,就可以通過事件的方式完成watcher任務。
參考程式碼:
'use strict';
var fs = require('fs');
var path = require('path');
var os = require('os');
var watchList = {};
var timer = {};
var walk = function (dir, callback, filter) {
fs.readdirSync(dir).forEach(function (item) {
var fullname = path.join(dir, item);
if (fs.statSync(fullname).isDirectory()){
if (!filter(fullname)){
return;
}
watch(fullname, callback, filter);
walk(fullname, callback, filter);
}
});
};
var watch = function (name, callback, filter) {
if (watchList[name]) {
watchList[name].close();
}
watchList[name] = fs.watch(name, function (event, filename) {
if (filename === null) {
return;
}
var fullname = path.join(name, filename);
var type;
var fstype;
if (!filter(fullname)) {
return;
}
// 檢查檔案、目錄是否存在
if (!fs.existsSync(fullname)) {
// 如果目錄被刪除則關閉監視器
if (watchList[fullname]) {
fstype = 'directory';
watchList[fullname].close();
delete watchList[fullname];
} else {
fstype = 'file';
}
type = 'delete';
} else {
// 檔案
if (fs.statSync(fullname).isFile()) {
fstype = 'file';
type = event == 'rename' ? 'create' : 'updated';
// 資料夾
} else if (event === 'rename') {
fstype = 'directory';
type = 'create';
watch(fullname, callback, filter);
walk(fullname, callback, filter);
}
}
var eventData = {
type: type,
target: filename,
parent: parent,
fstype: fstype
};
if (/windows/i.test(os.type())) {
// window 下的相容處理
clearTimeout(timer[fullname]);
timer[fullname] = setTimeout(function() {
callback(eventData);
}, 16);
} else {
callback(eventData);
}
});
};
/**
* @param {String} 要監聽的目錄
* @param {Function} 檔案、目錄改變後的回撥函式
* @param {Function} 過濾器(可選)
*/
module.exports = function (dir, callback, filter) {
// 排除“.”、“_”開頭或者非英文命名的目錄
var FILTER_RE = /[^\w\.\-$]/;
filter = filter || function (name) {
return !FILTER_RE.test(name);
};
watch(dir, callback, filter);
walk(dir, callback, filter);
};