node实现watcher的困境

@(node,watcher)

watcher,在如今的前端领域已经数见不鲜了。目前流行的gulp流程工具提供了watcher的选项,是我们在开发过程中不需要手动进行触发构建流程,转而根据文件(目录)内容改变来触发。

深入到watcher实现层,其实是基于node的fs.watch API,但是fs.watch有很多“不确定性”,下文会一一解答。


[TOC]

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

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏惨绿少年

rsync 服务部署详解

第1章 rsync 软件介绍 1.1 什么是rsync rsync 是一款开源的、快速的、多功能的、可实现全量及增量的本地或远程数据同步备份的优秀工具。 ht...

52200
来自专栏bboysoul

ubuntu安装sql-server

微软在2016年宣布sqlserver支持linux,目前支持在docker,ubuntu,,centos,suse上安装,安装过程也很简单,就是添加软件源然后...

20840
来自专栏IMWeb前端团队

thinkjs学习笔记

thinkjs 开始 安装 npm install -g thinkjs-cmd 查看是否安装成功 thinkjs -v 新建项目 mkdir new_dir_...

30770
来自专栏乐百川的学习频道

安装和使用MongoDB

MongoDB是一个著名的NoSQL数据库,顾名思义就是不使用SQL的数据库,目前在很多场景都有使用。如果你不喜欢使用笨拙的各种SQL数据库,可以尝试使用一下M...

1.1K60
来自专栏流柯技术学院

linux下MySQL表名忽略大小写设置

最近公司项目的MySQL数据库要迁移到linux下,部署时日志总是显示报找不到一个表,用MYSQL查看明明有这个表。后来经百度,原来LINUX下的MYSQL默认...

34220
来自专栏塔奇克马敲代码

Windows环境下的RTKPlot_Qt版本编译时遇到的问题和解决方法

22350
来自专栏Jerry的SAP技术分享

使用SAP云平台的destination消费Internet上的OData service

通过SAP云平台上的destination我们可以消费Internet上的OData service或者其他通过HTTP方式暴露出来的服务。

42240
来自专栏北京马哥教育

Varnish 4.0 实战

简介 Varnish 是一款高性能且开源的反向代理服务器和 HTTP 加速器,其采用全新的软件体系机构,和现在的硬件体系紧密配合,与传统的 squid 相比,v...

46140
来自专栏java学习

面试题13(一个具有生命的线程有哪些状态)

考点:考察求职者对线程的理解 出现频率:★★★ 【面试题解析】线程的状态表示线程在某时间段内进行的活动和将要进行的任务程有创建、就绪、运行、阻塞、死亡5种状态。...

34850
来自专栏木头编程 - moTzxx

ThinkPHP 框架下支付宝支付

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/u011415782/article/de...

50630

扫码关注云+社区

领取腾讯云代金券