@(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);
};