前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >node中文件监听的实现

node中文件监听的实现

原创
作者头像
brzhang
修改2020-08-04 10:15:07
1.4K0
修改2020-08-04 10:15:07
举报
文章被收录于专栏:玩转全栈

在做前端开发的过程中,我们不免要使用到liveServer这样的功能,很常见的,在我们开发Vue或者React应用的过程中,我们一般会启动一个devServer,然后,开发的时候,改动js等文件,所打开的网页就刷新了,难道你从来没有考虑过,这样的事情是如何发生的吗?

没错,这就是今天的主角:chokidarA neat wrapper around Node.js fs.watch / fs.watchFile / FSEvents.

代码语言:txt
复制
const chokidar = require('chokidar');

// One-liner for current directory
chokidar.watch('.').on('all', (event, path) => {
  console.log(event, path);
});

这货使用起来非常方便,但是devServer实现改动代码后,你保存文件,网页那边跟着刷新其实核心原理就是这个。

这里为了简单起见,我们看一个简单版的liveServer,源码在此。我们可以看到:

代码语言:txt
复制
// Setup file watcher
	LiveServer.watcher = chokidar.watch(watchPaths, {
		ignored: ignored,
		ignoreInitial: true,
		disableGlobbing: disableGlobbing
	});

	function handleChange(changePath) {
		var cssChange = path.extname(changePath) === ".css";
		if (LiveServer.logLevel >= 1) {
			if (cssChange)
				console.log("CSS change detected".magenta, changePath);
			else console.log("Change detected".cyan, changePath);
		}
		clients.forEach(function (ws) {
			if (ws)
				ws.send((cssChange && !fullReload) ? 'refreshcss' : 'reload');
		});
	}

	//return server;


	LiveServer.watcher
		.on("change", handleChange)
		.on("add", handleChange)
		.on("unlink", handleChange)
		.on("addDir", handleChange)
		.on("unlinkDir", handleChange)
		.on("ready", function () {
			if (LiveServer.logLevel >= 1)
				console.log("Ready for changes".cyan);
			if (callback) {
				callback();
			}
		})
		.on("error", function (err) {
			console.log("ERROR:".red, err);
		});

里面有这样的一段逻辑,这个其实就是代码变更触发整个页面刷新逻辑的主体了。那么,我们的这个watch是如何实现的呢?下面就让我们一层层剥开这个库的神秘面纱吧。

代码语言:txt
复制
add(paths_, _origAdd, _internal) {
  const {cwd, disableGlobbing} = this.options;
  this.closed = false;
  let paths = unifyPaths(paths_);
  if (cwd) {
    paths = paths.map((path) => {
      const absPath = getAbsolutePath(path, cwd);
      // Check `path` instead of `absPath` because the cwd portion can't be a glob
      if (disableGlobbing || !isGlob(path)) {
        return absPath;
      }
      return normalizePath(absPath);
    });
  }

  if (this.options.useFsEvents && this._fsEventsHandler) {
   ....
  }

  return this;
}

首先,我们看下add这个方法,这个方法返回this,也就是watch本身,在看看构造函数;

代码语言:txt
复制
class FSWatcher extends EventEmitter {
constructor(_opts) {
  super();

  const opts = {};
  if (_opts) Object.assign(opts, _opts); // for frozen objects

  /** @type {Map<String, DirEntry>} */
  this._watched = new Map();
  /** @type {Map<String, Array>} */
  this._closers = new Map();
  /** @type {Set<String>} */
  this._ignoredPaths = new Set();

  /** @type {Map<ThrottleType, Map>} */
  this._throttled = new Map();

  /** @type {Map<Path, String|Boolean>} */
  this._symlinkPaths = new Map();

  this._streams = new Set();
  this.closed = false;

 ......
  this._emitReady = () => {
    readyCalls++;
    if (readyCalls >= this._readyCount) {
      this._emitReady = EMPTY_FN;
      this._readyEmitted = true;
      // use process.nextTick to allow time for listener to be bound
      process.nextTick(() => this.emit(EV_READY));
    }
  };
  this._emitRaw = (...args) => this.emit(EV_RAW, ...args);
  this._readyEmitted = false;
  this.options = opts;

  // Initialize with proper watcher.
  if (opts.useFsEvents) {
    this._fsEventsHandler = new FsEventsHandler(this);
  } else {
    this._nodeFsHandler = new NodeFsHandler(this);
  }

  // You’re frozen when your heart’s not open.
  Object.freeze(opts);
}

我们发现它是继承自EventEmitter,这意味着他可以发送事件和注册监听事件。嗯,似乎明白了,文件更改之后发送一个事件而已。然后它这里定义了__watched,_ignoredPaths等变量这意味着我们配置反向规则。

然后useFsEvents选项决定使用FsEventsHandler还是使用NodeFsHandler,显然,我们紧紧看一种就能了解这个逻辑了,比如就看FsEventsHandler。

接下来,最为关键的是,我们对文件的修改是可以说是操作系统上做的一些事情,那么,这些个事件是如何传达到给我们的watcher呢?

实际上,是因为这么一个库起到了关键作用(c语言实现的),我们看他的描述:

Native access to MacOS FSEvents in Node.js

The FSEvents API in MacOS allows applications to register for notifications of changes to a given directory tree. It is a very fast and lightweight alternative to kqueue.

This is a low-level library. For a cross-platform file watching module that uses fsevents, check out Chokidar.

同时,我们在FsEventsHandler中可以看到这么一段代码是为了初始化这个fsevents的。

代码语言:txt
复制
let fsevents;
try {
  fsevents = require('fsevents');
} catch (error) {
  if (process.env.CHOKIDAR_PRINT_FSEVENTS_REQUIRE_ERROR) console.error(error);
}

if (fsevents) {
  // TODO: real check
  const mtch = process.version.match(/v(\d+)\.(\d+)/);
  if (mtch && mtch[1] && mtch[2]) {
    const maj = Number.parseInt(mtch[1], 10);
    const min = Number.parseInt(mtch[2], 10);
    if (maj === 8 && min < 16) {
      fsevents = undefined;
    }
  }
}

了解到fsevents的用法是:

代码语言:txt
复制
const fsevents = require('fsevents');
const stop = fsevents.watch(__dirname, (path, flags, id) => {
  const info = fsevents.getInfo(path, flags, id);
}); // To start observation
stop(); 

因此,我们去看看chokidir中是否有这么一段代码是监听底层文件操作的。

代码语言:txt
复制
/**
 * Instantiates the fsevents interface
 * @param {Path} path path to be watched
 * @param {Function} callback called when fsevents is bound and ready
 * @returns {{stop: Function}} new fsevents instance
 */
const createFSEventsInstance = (path, callback) => {
  const stop = fsevents.watch(path, callback);
  return {stop};
};

轻松找到这段代码,然后,看看谁调用了这个createFSEventsInstance。

代码语言:txt
复制
 if (cont || watchedParent) {
    cont.listeners.add(filteredListener);
  } else {
    cont = {
      listeners: new Set([filteredListener]),
      rawEmitter,
      watcher: createFSEventsInstance(watchPath, (fullPath, flags) => {
        if (!cont.listeners.size) return;
        const info = fsevents.getInfo(fullPath, flags);
        cont.listeners.forEach(list => {
          list(fullPath, flags, info);
        });

        cont.rawEmitter(info.event, fullPath, info);
      })
    };
    FSEventsWatchers.set(watchPath, cont);
  }

cont.rawEmitter(info.event, fullPath, info);关键代码,这里就是将监听到的底层文件操作事件捕捉并传递了出来。

自此整个脉络就清晰了。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档