chokidar、gaze、node-watch 和 watchpack 都是用于在 Node.js 环境中监听文件系统变化的工具库,它们封装了底层的 fs.watch 或 fs.watchFile,以提供更稳定、跨平台、功能丰富的文件变更通知机制。这类库广泛应用于构建工具(如 Webpack、Vite)、开发服务器热重载、自动化脚本等场景,解决原生 API 在不同操作系统下行为不一致、事件触发不稳定或缺失等问题。
在前端工程化中,监听文件变化是开发服务器热更新、构建工具增量编译等核心功能的基础。Node.js 原生的 fs.watch 虽然可用,但在不同操作系统下行为不一致(例如 macOS 对大小写不敏感、Windows 事件合并、Linux inotify 限制等),且容易漏报或误报。为此,社区涌现出多个封装库。本文将从实际工程角度,深入比较 chokidar、gaze、node-watch 和 watchpack 的技术细节与适用场景。
首先明确:gaze 已被官方废弃。在其 npm 页面 和 GitHub 仓库 中均有明确提示,建议用户迁移到 chokidar。因此,新项目绝不应选用 gaze,以下分析仅作历史参考。
所有库都提供类似 EventEmitter 的接口,通过 add/watch 添加监听路径,通过 on('change', ...) 监听事件。但细节差异显著。
chokidar:功能最全的行业标准chokidar 提供高度可配置的监听器,支持 glob 模式、忽略规则、等待文件写入完成等。
// chokidar
const chokidar = require('chokidar');
const watcher = chokidar.watch('src/**/*.js', {
ignored: /(^|[\/\\])\../, // 忽略 . 开头的文件
persistent: true,
awaitWriteFinish: {
stabilityThreshold: 2000,
pollInterval: 100
}
});
watcher.on('add', path => console.log('File added:', path));
watcher.on('change', path => console.log('File changed:', path));
watcher.on('unlink', path => console.log('File removed:', path));
gaze:已废弃的旧方案gaze 使用 glob 模式,但 API 较老,且不处理文件写入未完成的问题。
// gaze (已废弃,仅作示例)
const gaze = require('gaze');
gaze('src/**/*.js', function(err, watcher) {
this.on('all', function(event, filepath) {
console.log(event, filepath); // 'added', 'changed', 'deleted'
});
});
node-watch:轻量简洁node-watch API 极简,支持递归监听和过滤函数,但不内置 glob 支持(需自行处理)。
// node-watch
const watch = require('node-watch');
const watcher = watch('src', { recursive: true }, function(evt, name) {
console.log(evt, name); // 'update', 'remove'
});
// 或使用过滤函数
watch('src', {
recursive: true,
filter: (f) => !/\.tmp$/.test(f)
}, callback);
watchpack:Webpack 定制化方案watchpack 设计用于大规模项目,需显式调用 watch 方法并传入文件/目录列表,支持时间戳和文件哈希比对。
// watchpack
const Watchpack = require('watchpack');
const wp = new Watchpack({
aggregateTimeout: 300, // 合并事件间隔
followSymlinks: true
});
wp.watch({
files: ['package.json'],
directories: ['src']
}, () => {
// 回调在变化后触发
const changes = wp.getAggregated();
console.log('Changed files:', changes.changedFiles);
console.log('Removed files:', changes.removedFiles);
});
chokidar:通过 fsevents(macOS)、inotify(Linux)和 ReadDirectoryChangesW(Windows)等原生绑定,最大程度保证各平台行为一致。默认启用 usePolling: false,在大多数场景下高效可靠;若遇权限问题可回退到轮询模式。node-watch:主要封装 fs.watch,在 macOS 和 Windows 上表现尚可,但在 Linux 下可能因 inotify 限制(如监控大量文件)而失效,此时需手动启用轮询(usePolling: true)。watchpack:继承 Webpack 的实战经验,对大型项目(数千文件)做了优化,内部使用 chokidar 作为底层(v2+),因此兼容性与 chokidar 一致。gaze:依赖 fs.watch 和 fs.watchFile,在旧版 Node.js 中存在较多平台问题,且无后续修复。chokidar:原生支持字符串、正则、函数或数组形式的 ignored 选项,且与 glob 模式无缝集成。node-watch:仅支持 filter 函数,需自行实现 glob 匹配逻辑。watchpack:通过 ignored 选项支持正则或函数,但不直接处理 glob。gaze:支持 glob 模式中的排除语法(如 !src/*.spec.js),但灵活性有限。编辑器保存大文件时,fs.watch 可能多次触发事件。chokidar 的 awaitWriteFinish 选项可等待文件稳定后再触发,避免中间状态干扰。
// chokidar 特有
chokidar.watch('file.txt', {
awaitWriteFinish: true
}).on('change', path => {
// 仅在文件写入完成后触发
});
其他库均无此功能,需自行实现防抖或延时逻辑。
chokidar 和 watchpack 通过原生绑定和事件聚合显著优于纯 JavaScript 方案。node-watch 在启用轮询时 CPU 占用较高。watchpack 额外维护文件时间戳和哈希缓存,适合需要精确判断内容是否变化的场景(如 Webpack 的缓存失效)。chokidar:因其稳定性、功能完整性和社区支持,已成为事实标准(被 Vite、Rollup、Jest 等广泛采用)。node-watch:若项目简单、无需 glob 或高级忽略规则,其零依赖(除 Node.js 外)和简洁 API 是优势。watchpack:若需与 Webpack 的缓存机制协同工作,直接使用其内部监听层可避免重复监听和状态不一致。gaze:立即迁移到 chokidar,API 差异可通过少量代码调整解决。| 特性 | chokidar | gaze (废弃) | node-watch | watchpack |
|---|---|---|---|---|
| 维护状态 | ✅ 活跃 | ❌ 已废弃 | ✅ 活跃 | ✅ 活跃 (Webpack 生态) |
| 跨平台兼容性 | ⭐⭐⭐⭐⭐ | ⭐⭐ | ⭐⭐⭐ | ⭐⭐⭐⭐⭐ (基于 chokidar) |
| Glob 模式支持 | ✅ 原生 | ✅ 基础 | ❌ 需自行实现 | ❌ |
| 忽略规则 | ✅ 灵活 | ⚠️ 有限 | ⚠️ 仅函数过滤 | ✅ 正则/函数 |
| 写入完成检测 | ✅ awaitWriteFinish | ❌ | ❌ | ❌ |
| 大规模性能 | ⭐⭐⭐⭐ | ⭐ | ⭐⭐ | ⭐⭐⭐⭐⭐ |
| 典型用户 | Vite, Jest, Rollup | — | 小型工具 | Webpack |
chokidar:它解决了绝大多数文件监听痛点,且经过大规模验证。node-watch。watchpack。gaze。文件监听看似简单,实则充满平台差异和边缘情况。选择一个成熟、活跃维护的库,能让你避免陷入调试 fs.watch 的泥潭,把精力集中在核心业务上。
选择 watchpack 如果你正在开发或深度定制基于 Webpack 的构建系统,或需要与 Webpack 的缓存和增量编译机制紧密集成。它是 Webpack 内部使用的文件监听层,针对大规模项目优化了性能,但作为通用库其文档和独立使用体验不如 chokidar,通常不建议在非 Webpack 生态中直接使用。
不要在新项目中使用 gaze,因为该包已在 npm 上被官方标记为废弃(deprecated),其 GitHub 仓库也已归档。虽然它曾提供基于 glob 模式的监听能力,但缺乏现代维护和 bug 修复,应优先评估 chokidar 或 node-watch 等活跃替代品。
选择 chokidar 如果你需要一个功能全面、跨平台兼容性极佳、社区广泛采用且持续维护的文件监听方案。它特别适合构建工具、CLI 工具或任何对可靠性要求高的生产环境,支持深度目录监听、忽略规则、延迟触发等高级特性,并能自动处理 macOS、Linux 和 Windows 的差异。
选择 node-watch 如果你追求轻量级、API 简洁且不需要复杂配置的监听需求。它直接封装 fs.watch 并修复了部分平台问题,支持递归监听和基本过滤,适合小型脚本或对依赖体积敏感的场景,但功能不如 chokidar 丰富,且在极端边缘情况下的稳定性略逊一筹。
Wrapper library for directory and file watching.
watchpack high level API doesn't map directly to watchers. Instead a three level architecture ensures that for each directory only a single watcher exists.
DirectoryWatchers from a WatcherManager, which ensures that only a single DirectoryWatcher per directory is created.Watcher can be obtained from a DirectoryWatcher and provides a filtered view on the DirectoryWatcher.DirectoryWatcher and Watcher to decide when to close them.DirectoryWatcher.const Watchpack = require("watchpack");
const wp = new Watchpack({
// options:
aggregateTimeout: 1000,
// fire "aggregated" event when after a change for 1000ms no additional change occurred
// aggregated defaults to undefined, which doesn't fire an "aggregated" event
poll: true,
// poll: true - use polling with the default interval
// poll: 10000 - use polling with an interval of 10s
// poll defaults to undefined, which prefer native watching methods
// Note: enable polling when watching on a network path
// When WATCHPACK_POLLING environment variable is set it will override this option
followSymlinks: true,
// true: follows symlinks and watches symlinks and real files
// (This makes sense when symlinks has not been resolved yet, comes with a performance hit)
// false (default): watches only specified item they may be real files or symlinks
// (This makes sense when symlinks has already been resolved)
ignored: "**/.git",
// ignored: "string" - a glob pattern for files or folders that should not be watched
// ignored: ["string", "string"] - multiple glob patterns that should be ignored
// ignored: /regexp/ - a regular expression for files or folders that should not be watched
// ignored: (entry) => boolean - an arbitrary function which must return truthy to ignore an entry
// For all cases expect the arbitrary function the path will have path separator normalized to '/'.
// All subdirectories are ignored too
});
// Watchpack.prototype.watch({
// files: Iterable<string>,
// directories: Iterable<string>,
// missing: Iterable<string>,
// startTime?: number
// })
wp.watch({
files: listOfFiles,
directories: listOfDirectories,
missing: listOfNotExistingItems,
startTime: Date.now() - 10000,
});
// starts watching these files and directories
// calling this again will override the files and directories
// files: can be files or directories, for files: content and existence changes are tracked
// for directories: only existence and timestamp changes are tracked
// directories: only directories, directory content (and content of children, ...) and
// existence changes are tracked.
// assumed to exist, when directory is not found without further information a remove event is emitted
// missing: can be files or directories,
// only existence changes are tracked
// expected to not exist, no remove event is emitted when not found initially
// files and directories are assumed to exist, when they are not found without further information a remove event is emitted
// missing is assumed to not exist and no remove event is emitted
wp.on("change", (filePath, mtime, explanation) => {
// filePath: the changed file
// mtime: last modified time for the changed file
// explanation: textual information how this change was detected
});
wp.on("remove", (filePath, explanation) => {
// filePath: the removed file or directory
// explanation: textual information how this change was detected
});
wp.on("aggregated", (changes, removals) => {
// changes: a Set of all changed files
// removals: a Set of all removed files
// watchpack gives up ownership on these Sets.
});
// Watchpack.prototype.pause()
wp.pause();
// stops emitting events, but keeps watchers open
// next "watch" call can reuse the watchers
// The watcher will keep aggregating events
// which can be received with getAggregated()
// Watchpack.prototype.close()
wp.close();
// stops emitting events and closes all watchers
// Watchpack.prototype.getAggregated(): { changes: Set<string>, removals: Set<string> }
const { changes, removals } = wp.getAggregated();
// returns the current aggregated info and removes that from the watcher
// The next aggregated event won't include that info and will only emitted
// when futher changes happen
// Can also be used when paused.
// Watchpack.prototype.collectTimeInfoEntries(fileInfoEntries: Map<string, Entry>, directoryInfoEntries: Map<string, Entry>)
wp.collectTimeInfoEntries(fileInfoEntries, directoryInfoEntries);
// collects time info objects for all known files and directories
// this include info from files not directly watched
// key: absolute path, value: object with { safeTime, timestamp }
// safeTime: a point in time at which it is safe to say all changes happened before that
// timestamp: only for files, the mtime timestamp of the file
// Watchpack.prototype.getTimeInfoEntries()
const fileTimes = wp.getTimeInfoEntries();
// returns a Map with all known time info objects for files and directories
// similar to collectTimeInfoEntries but returns a single map with all entries
// (deprecated)
// Watchpack.prototype.getTimes()
const fileTimesOld = wp.getTimes();
// returns an object with all known change times for files
// this include timestamps from files not directly watched
// key: absolute path, value: timestamp as number