node實現watcher的困境

@(node,watcher)javascript

watcher,在現在的前端領域已經數見不鮮了。目前流行的gulp流程工具提供了watcher的選項,是咱們在開發過程當中不須要手動進行觸發構建流程,轉而根據文件(目錄)內容改變來觸發。前端

深刻到watcher實現層,實際上是基於node的fs.watch API,可是fs.watch有不少「不肯定性」,下文會一一解答。java


[TOC]node

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也有兼容性問題,在使用時也要注意兼容性判斷。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);
};
相關文章
相關標籤/搜索