在做前端开发的过程中,我们不免要使用到liveServer这样的功能,很常见的,在我们开发Vue或者React应用的过程中,我们一般会启动一个devServer,然后,开发的时候,改动js等文件,所打开的网页就刷新了,难道你从来没有考虑过,这样的事情是如何发生的吗?
没错,这就是今天的主角:chokidar,A neat wrapper around Node.js fs.watch / fs.watchFile / FSEvents.
const chokidar = require('chokidar');
// One-liner for current directory
chokidar.watch('.').on('all', (event, path) => {
console.log(event, path);
});
这货使用起来非常方便,但是devServer实现改动代码后,你保存文件,网页那边跟着刷新其实核心原理就是这个。
这里为了简单起见,我们看一个简单版的liveServer,源码在此。我们可以看到:
// 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是如何实现的呢?下面就让我们一层层剥开这个库的神秘面纱吧。
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本身,在看看构造函数;
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的。
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的用法是:
const fsevents = require('fsevents');
const stop = fsevents.watch(__dirname, (path, flags, id) => {
const info = fsevents.getInfo(path, flags, id);
}); // To start observation
stop();
因此,我们去看看chokidir中是否有这么一段代码是监听底层文件操作的。
/**
* 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。
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 删除。