watchpack vs gaze vs chokidar vs node-watch
Node.js 文件系统监听库选型指南
watchpackgazechokidarnode-watch类似的npm包:

Node.js 文件系统监听库选型指南

chokidargazenode-watchwatchpack 都是用于在 Node.js 环境中监听文件系统变化的工具库,它们封装了底层的 fs.watchfs.watchFile,以提供更稳定、跨平台、功能丰富的文件变更通知机制。这类库广泛应用于构建工具(如 Webpack、Vite)、开发服务器热重载、自动化脚本等场景,解决原生 API 在不同操作系统下行为不一致、事件触发不稳定或缺失等问题。

npm下载趋势

3 年

GitHub Stars 排名

统计详情

npm包名称
下载量
Stars
大小
Issues
发布时间
License
watchpack46,732,91439695.7 kB105 个月前MIT
gaze2,475,7561,154-688 年前MIT
chokidar012,13282.1 kB446 个月前MIT
node-watch034226.1 kB83 年前MIT

Node.js 文件监听库深度对比:chokidar vs gaze vs node-watch vs watchpack

在前端工程化中,监听文件变化是开发服务器热更新、构建工具增量编译等核心功能的基础。Node.js 原生的 fs.watch 虽然可用,但在不同操作系统下行为不一致(例如 macOS 对大小写不敏感、Windows 事件合并、Linux inotify 限制等),且容易漏报或误报。为此,社区涌现出多个封装库。本文将从实际工程角度,深入比较 chokidargazenode-watchwatchpack 的技术细节与适用场景。

⚠️ 废弃状态警示

首先明确:gaze 已被官方废弃。在其 npm 页面GitHub 仓库 中均有明确提示,建议用户迁移到 chokidar。因此,新项目绝不应选用 gaze,以下分析仅作历史参考。

🛠️ 基础用法与 API 设计

所有库都提供类似 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.watchfs.watchFile,在旧版 Node.js 中存在较多平台问题,且无后续修复。

🧩 高级功能对比

忽略规则(Ignore Patterns)

  • chokidar:原生支持字符串、正则、函数或数组形式的 ignored 选项,且与 glob 模式无缝集成。
  • node-watch:仅支持 filter 函数,需自行实现 glob 匹配逻辑。
  • watchpack:通过 ignored 选项支持正则或函数,但不直接处理 glob。
  • gaze:支持 glob 模式中的排除语法(如 !src/*.spec.js),但灵活性有限。

文件写入完成检测(Await Write Finish)

编辑器保存大文件时,fs.watch 可能多次触发事件。chokidarawaitWriteFinish 选项可等待文件稳定后再触发,避免中间状态干扰。

// chokidar 特有
chokidar.watch('file.txt', {
  awaitWriteFinish: true
}).on('change', path => {
  // 仅在文件写入完成后触发
});

其他库均无此功能,需自行实现防抖或延时逻辑。

性能与资源占用

  • 对于小项目(<100 文件):四者差异不大。
  • 对于中大型项目(>1000 文件):chokidarwatchpack 通过原生绑定和事件聚合显著优于纯 JavaScript 方案。node-watch 在启用轮询时 CPU 占用较高。
  • watchpack 额外维护文件时间戳和哈希缓存,适合需要精确判断内容是否变化的场景(如 Webpack 的缓存失效)。

🏗️ 典型应用场景

场景 1:通用开发工具(如 CLI、脚手架)

  • 首选 chokidar:因其稳定性、功能完整性和社区支持,已成为事实标准(被 Vite、Rollup、Jest 等广泛采用)。

场景 2:轻量级脚本或嵌入式监听

  • 考虑 node-watch:若项目简单、无需 glob 或高级忽略规则,其零依赖(除 Node.js 外)和简洁 API 是优势。

场景 3:Webpack 插件或自定义构建系统

  • 使用 watchpack:若需与 Webpack 的缓存机制协同工作,直接使用其内部监听层可避免重复监听和状态不一致。

场景 4:遗留项目迁移

  • 避免 gaze:立即迁移到 chokidar,API 差异可通过少量代码调整解决。

📊 总结对比表

特性chokidargaze (废弃)node-watchwatchpack
维护状态✅ 活跃❌ 已废弃✅ 活跃✅ 活跃 (Webpack 生态)
跨平台兼容性⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐ (基于 chokidar)
Glob 模式支持✅ 原生✅ 基础❌ 需自行实现
忽略规则✅ 灵活⚠️ 有限⚠️ 仅函数过滤✅ 正则/函数
写入完成检测awaitWriteFinish
大规模性能⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐
典型用户Vite, Jest, Rollup小型工具Webpack

💡 最终建议

  • 90% 的新项目应选择 chokidar:它解决了绝大多数文件监听痛点,且经过大规模验证。
  • 仅当项目极度轻量且无需高级功能时考虑 node-watch
  • 若深度集成 Webpack,直接使用 watchpack
  • 永远避开 gaze

文件监听看似简单,实则充满平台差异和边缘情况。选择一个成熟、活跃维护的库,能让你避免陷入调试 fs.watch 的泥潭,把精力集中在核心业务上。

如何选择: watchpack vs gaze vs chokidar vs node-watch

  • watchpack:

    选择 watchpack 如果你正在开发或深度定制基于 Webpack 的构建系统,或需要与 Webpack 的缓存和增量编译机制紧密集成。它是 Webpack 内部使用的文件监听层,针对大规模项目优化了性能,但作为通用库其文档和独立使用体验不如 chokidar,通常不建议在非 Webpack 生态中直接使用。

  • gaze:

    不要在新项目中使用 gaze,因为该包已在 npm 上被官方标记为废弃(deprecated),其 GitHub 仓库也已归档。虽然它曾提供基于 glob 模式的监听能力,但缺乏现代维护和 bug 修复,应优先评估 chokidarnode-watch 等活跃替代品。

  • chokidar:

    选择 chokidar 如果你需要一个功能全面、跨平台兼容性极佳、社区广泛采用且持续维护的文件监听方案。它特别适合构建工具、CLI 工具或任何对可靠性要求高的生产环境,支持深度目录监听、忽略规则、延迟触发等高级特性,并能自动处理 macOS、Linux 和 Windows 的差异。

  • node-watch:

    选择 node-watch 如果你追求轻量级、API 简洁且不需要复杂配置的监听需求。它直接封装 fs.watch 并修复了部分平台问题,支持递归监听和基本过滤,适合小型脚本或对依赖体积敏感的场景,但功能不如 chokidar 丰富,且在极端边缘情况下的稳定性略逊一筹。

watchpack的README

watchpack

Wrapper library for directory and file watching.

Test Codecov Downloads

Concept

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.

  • The high level API requests DirectoryWatchers from a WatcherManager, which ensures that only a single DirectoryWatcher per directory is created.
  • A user-faced Watcher can be obtained from a DirectoryWatcher and provides a filtered view on the DirectoryWatcher.
  • Reference-counting is used on the DirectoryWatcher and Watcher to decide when to close them.
  • The real watchers are created by the DirectoryWatcher.
  • Files are never watched directly. This should keep the watcher count low.
  • Watching can be started in the past. This way watching can start after file reading.
  • Symlinks are not followed, instead the symlink is watched.

API

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