@(node,watcher)javascript
watcher,在現在的前端領域已經數見不鮮了。目前流行的gulp流程工具提供了watcher的選項,是咱們在開發過程當中不須要手動進行觸發構建流程,轉而根據文件(目錄)內容改變來觸發。前端
深刻到watcher實現層,實際上是基於node的fs.watch API,可是fs.watch有不少「不肯定性」,下文會一一解答。java
[TOC]node
(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也有兼容性問題,在使用時也要注意兼容性判斷。linux
在上一節中簡單介紹了watch API,也簡單提到了一些兼容性問題,在此列舉出來:gulp
recursive屬性在linux下失效;windows
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, 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); };