chokidar、gaze、node-watch、nodemon 和 watch 都是 Node.js 生态中用于监听文件系统变化的工具,但它们的定位和适用场景差异巨大。chokidar 是目前社区事实上的标准跨平台监听库,提供了稳定且一致的事件接口。gaze 和 watch 是早期的监听方案,目前大多已停止维护或不再推荐用于新项目。node-watch 是对原生 fs.watch 的轻量级封装,适合简单场景。nodemon 则是一个专门用于开发环境中自动重启 Node.js 应用的工具,虽然底层涉及文件监听,但其核心目的是进程管理而非通用文件监控。
在 Node.js 开发中,监听文件变化是构建工具、热重载(HMR)和自动化任务的核心需求。虽然表面上看这些包都在做同一件事,但它们的底层实现、稳定性和适用场景截然不同。作为架构师,我们需要清楚区分“通用监听库”与“开发工具”,并识别哪些是历史遗留产物。
首先必须明确,nodemon 与其他四个包不在同一个类别。
chokidar、gaze、node-watch、watch 是通用文件监听库。
nodemon 是开发辅助工具。
// nodemon: 专注于重启进程,而非暴露文件事件
const nodemon = require('nodemon');
nodemon({
script: 'app.js',
watch: ['src/'],
ext: 'js,json'
});
nodemon.on('restart', () => {
console.log('应用已重启');
});
// 注意:它没有提供类似 'fileChanged' 的通用事件供你处理业务逻辑
// chokidar: 专注于暴露文件事件,供你编写业务逻辑
const chokidar = require('chokidar');
const watcher = chokidar.watch('src/', {
ignored: /node_modules/,
persistent: true
});
watcher.on('change', path => {
console.log(`文件 ${path} 已修改,触发构建`);
// 这里可以执行任意逻辑,如打包、部署等
});
不同的库在事件命名和参数传递上有显著差异,这直接影响代码的可读性和迁移成本。
chokidar 使用语义清晰的事件名(add, addDir, change, unlink, unlinkDir)。
// chokidar: 语义化的事件区分
watcher.on('add', path => console.log(`新建文件:${path}`));
watcher.on('unlink', path => console.log(`删除文件:${path}`));
watcher.on('change', path => console.log(`文件变动:${path}`));
node-watch 的事件类型较为简单(update, remove)。
// node-watch: 通过事件对象传递信息
watch('src/', { recursive: true }, function(evt, name) {
console.log('%s %s', evt, name);
// evt 可能是 'update' 或 'remove'
});
gaze 使用 changed, added, deleted 事件。
chokidar。// gaze: 旧式回调风格
const gaze = require('gaze');
gaze(['**/*.js'], function(err, watcher) {
watcher.on('changed', function(filepath) {
console.log('文件被修改', filepath);
});
watcher.on('added', function(filepath) {
console.log('文件被添加', filepath);
});
});
watch (mikeal/watch) 的 API 非常原始。
watch.watchTree 方法。// watch: 原始的文件树监听
const watch = require('watch');
watch.watchTree('src/', function(f, curr, prev) {
if (typeof f == "object" && prev === null && curr === null) {
// 遍历完成
} else if (prev === null) {
console.log('新文件', f);
} else {
console.log('文件修改', f);
}
});
文件监听的性能直接影响开发体验,特别是在大型单体仓库(Monorepo)中。
chokidar 优先使用系统原生的 fs.watch,并在检测到不可靠时自动降级到 fs.watchFile(轮询)。
awaitWatcher 等机制确保事件处理完成。// chokidar: 自动处理底层差异,支持持久化监听
const watcher = chokidar.watch('project/', {
persistent: true,
usePolling: false, // 默认优先使用原生事件
interval: 100 // 仅在轮询模式下生效
});
node-watch 同样是对原生的封装,但策略更简单。
fs.watch 的某些 bug,但功能集较小。// node-watch: 简单的递归选项
watch('project/', { recursive: true }, function(evt, name) {
// 在高负载下可能不如 chokidar 稳定
});
gaze 和 watch 在旧版本 Node.js 上表现尚可,但在新版 V8 和文件系统变更下已显疲态。
// gaze: 缺乏现代优化,可能导致高 CPU 占用
gaze('**/*', function() {
// 在大型项目中,回调触发频率难以控制
});
在架构选型中,库的维护状态是决定性因素。
chokidar 处于活跃维护状态。
nodemon 处于活跃维护状态。
gaze 和 watch 基本处于停滞状态。
node-watch 维护频率较低但相对稳定。
// 风险示例:使用已停止维护的库可能导致未来升级失败
// 不推荐
const watch = require('watch');
// 推荐
const chokidar = require('chokidar');
| 特性 | chokidar | node-watch | nodemon | gaze | watch |
|---|---|---|---|---|---|
| 定位 | 通用监听库 | 通用监听库 | 开发重启工具 | 通用监听库 (旧) | 通用监听库 (旧) |
| 维护状态 | ✅ 活跃 | ⚠️ 低频 | ✅ 活跃 | ❌ 停滞 | ❌ 停滞 |
| 跨平台稳定性 | 高 | 中 | 高 (仅限重启) | 低 | 低 |
| API 现代性 | 高 (Promise 友好) | 中 | 中 | 低 (回调) | 低 (回调) |
| 适用场景 | 构建工具、生产监控 | 简单脚本 | 本地开发调试 | 遗留项目维护 | 遗留项目维护 |
| 是否推荐 | 强烈推荐 | 可选 | 开发环境推荐 | 不推荐 | 不推荐 |
在设计前端工程化体系时,文件监听模块应当是透明且可靠的。
默认选择 chokidar:无论是编写自定义 CLI、构建插件还是后端同步服务,chokidar 都是最安全的选择。它的 API 设计允许你轻松处理防抖(debounce)和批量处理,这对于避免构建风暴至关重要。
隔离 nodemon:将 nodemon 限制在 package.json 的 devDependencies 和开发脚本中。不要让它渗透到你的核心业务逻辑或部署脚本里。
清理技术债务:如果代码库中还存在 gaze 或 watch,请制定迁移计划。它们的 API 差异不大,替换为 chokidar 通常只需修改导入语句和事件名称,但能显著提升长期稳定性。
关注原子写入:在 Linux 和 macOS 上,编辑器保存文件通常是“写入临时文件 -> 重命名”的过程。chokidar 能较好地处理这种 unlink + add 的组合事件,而老旧库可能会将其误判为文件删除,导致监听失效。
选择正确的工具不仅仅是为了代码能跑,更是为了在系统规模扩大时,减少不可预知的运维成本。
如果你的项目需要跨平台稳定运行,且对文件变动的准确性要求较高,chokidar 是首选。它解决了原生 fs.watch 在不同操作系统上的不一致问题,支持忽略模式、批量事件处理等高级功能。大多数现代构建工具(如 Vite、Webpack)底层都依赖它。
仅当你维护非常老旧的项目且无法迁移时使用 gaze。该库已不再积极维护,功能已被 chokidar 等更现代的方案超越。在新架构中引入它会增加技术债务,建议尽快替换。
如果你需要比原生 fs.watch 更好用但比 chokidar 更轻量的方案,可以选择 node-watch。它适合简单的脚本工具或对依赖体积敏感的场景,但在处理复杂目录结构时不如 chokidar 健壮。
如果你的目标是在开发过程中自动重启 Node.js 服务器,直接使用 nodemon。不要试图用它来做通用的文件监听任务,它的配置和事件系统都是为进程重启设计的,不适合构建流水线或数据同步场景。
避免在新项目中使用 watch 包。它是非常早期的实现,缺乏对现代 Node.js 特性的支持,且长期未更新。其功能完全可以通过 chokidar 或原生 API 更好地实现。
Minimal and efficient cross-platform file watching library
There are many reasons to prefer Chokidar to raw fs.watch / fs.watchFile in 2026:
renameatomic option
awaitWriteFinish option
Chokidar relies on the Node.js core fs module, but when using
fs.watch and fs.watchFile for watching, it normalizes the events it
receives, often checking for truth by getting file stats and/or dir contents.
The fs.watch-based implementation is the default, which
avoids polling and keeps CPU usage down. Be advised that chokidar will initiate
watchers recursively for everything within scope of the paths that have been
specified, so be judicious about not wasting system resources by watching much
more than needed. For some cases, fs.watchFile, which utilizes polling and uses more resources, is used.
Made for Brunch in 2012, it is now used in ~30 million repositories and has proven itself in production environments.
Install with npm:
npm install chokidar
Use it in your code:
import chokidar from 'chokidar';
// One-liner for current directory
chokidar.watch('.').on('all', (event, path) => {
console.log(event, path);
});
// Extended options
// ----------------
// Initialize watcher.
const watcher = chokidar.watch('file, dir, or array', {
ignored: (path, stats) => stats?.isFile() && !path.endsWith('.js'), // only watch js files
persistent: true,
});
// Something to use when events are received.
const log = console.log.bind(console);
// Add event listeners.
watcher
.on('add', (path) => log(`File ${path} has been added`))
.on('change', (path) => log(`File ${path} has been changed`))
.on('unlink', (path) => log(`File ${path} has been removed`));
// More possible events.
watcher
.on('addDir', (path) => log(`Directory ${path} has been added`))
.on('unlinkDir', (path) => log(`Directory ${path} has been removed`))
.on('error', (error) => log(`Watcher error: ${error}`))
.on('ready', () => log('Initial scan complete. Ready for changes'))
.on('raw', (event, path, details) => {
// internal
log('Raw event info:', event, path, details);
});
// 'add', 'addDir' and 'change' events also receive stat() results as second
// argument when available: https://nodejs.org/api/fs.html#fs_class_fs_stats
watcher.on('change', (path, stats) => {
if (stats) console.log(`File ${path} changed size to ${stats.size}`);
});
// Watch new files.
watcher.add('new-file');
watcher.add(['new-file-2', 'new-file-3']);
// Get list of actual paths being watched on the filesystem
let watchedPaths = watcher.getWatched();
// Un-watch some files.
await watcher.unwatch('new-file');
// Stop watching. The method is async!
await watcher.close().then(() => console.log('closed'));
// Full list of options. See below for descriptions.
// Do not use this example!
chokidar.watch('file', {
persistent: true,
// ignore .txt files
ignored: (file) => file.endsWith('.txt'),
// watch only .txt files
// ignored: (file, _stats) => _stats?.isFile() && !file.endsWith('.txt'),
awaitWriteFinish: true, // emit single event when chunked writes are completed
atomic: true, // emit proper events when "atomic writes" (mv _tmp file) are used
// The options also allow specifying custom intervals in ms
// awaitWriteFinish: {
// stabilityThreshold: 2000,
// pollInterval: 100
// },
// atomic: 100,
interval: 100,
binaryInterval: 300,
cwd: '.',
depth: 99,
followSymlinks: true,
ignoreInitial: false,
ignorePermissionErrors: false,
usePolling: false,
alwaysStat: false,
});
chokidar.watch(paths, [options])
paths (string or array of strings). Paths to files, dirs to be watched
recursively.options (object) Options object as defined below:persistent (default: true). Indicates whether the process
should continue to run as long as files are being watched.ignored function, regex, or path. Defines files/paths to be ignored.
The whole relative or absolute path is tested, not just filename. If a function with two arguments
is provided, it gets called twice per path - once with a single argument (the path), second
time with two arguments (the path and the
fs.Stats
object of that path).ignoreInitial (default: false). If set to false then add/addDir events are also emitted for matching paths while
instantiating the watching as chokidar discovers these file paths (before the ready event).followSymlinks (default: true). When false, only the
symlinks themselves will be watched for changes instead of following
the link references and bubbling events through the link's path.cwd (no default). The base directory from which watch paths are to be
derived. Paths emitted with events will be relative to this.usePolling (default: false).
Whether to use fs.watchFile (backed by polling), or fs.watch. If polling
leads to high CPU utilization, consider setting this to false. It is
typically necessary to set this to true to successfully watch files over
a network, and it may be necessary to successfully watch files in other
non-standard situations. Setting to true explicitly on MacOS overrides the
useFsEvents default. You may also set the CHOKIDAR_USEPOLLING env variable
to true (1) or false (0) in order to override this option.usePolling: true)
interval (default: 100). Interval of file system polling, in milliseconds. You may also
set the CHOKIDAR_INTERVAL env variable to override this option.binaryInterval (default: 300). Interval of file system
polling for binary files.
(see list of binary extensions)alwaysStat (default: false). If relying upon the
fs.Stats
object that may get passed with add, addDir, and change events, set
this to true to ensure it is provided even in cases where it wasn't
already available from the underlying watch events.depth (default: undefined). If set, limits how many levels of
subdirectories will be traversed.awaitWriteFinish (default: false).
By default, the add event will fire when a file first appears on disk, before
the entire file has been written. Furthermore, in some cases some change
events will be emitted while the file is being written. In some cases,
especially when watching for large files there will be a need to wait for the
write operation to finish before responding to a file creation or modification.
Setting awaitWriteFinish to true (or a truthy value) will poll file size,
holding its add and change events until the size does not change for a
configurable amount of time. The appropriate duration setting is heavily
dependent on the OS and hardware. For accurate detection this parameter should
be relatively high, making file watching much less responsive.
Use with caution.
options.awaitWriteFinish can be set to an object in order to adjust
timing params:awaitWriteFinish.stabilityThreshold (default: 2000). Amount of time in
milliseconds for a file size to remain constant before emitting its event.awaitWriteFinish.pollInterval (default: 100). File size polling interval, in milliseconds.ignorePermissionErrors (default: false). Indicates whether to watch files
that don't have read permissions if possible. If watching fails due to EPERM
or EACCES with this set to true, the errors will be suppressed silently.atomic (default: true if useFsEvents and usePolling are false).
Automatically filters out artifacts that occur when using editors that use
"atomic writes" instead of writing directly to the source file. If a file is
re-added within 100 ms of being deleted, Chokidar emits a change event
rather than unlink then add. If the default of 100 ms does not work well
for you, you can override it by setting atomic to a custom value, in
milliseconds.chokidar.watch() produces an instance of FSWatcher. Methods of FSWatcher:
.add(path / paths): Add files, directories for tracking.
Takes an array of strings or just one string..on(event, callback): Listen for an FS event.
Available events: add, addDir, change, unlink, unlinkDir, ready,
raw, error.
Additionally all is available which gets emitted with the underlying event
name and path for every event other than ready, raw, and error. raw is internal, use it carefully..unwatch(path / paths): Stop watching files or directories.
Takes an array of strings or just one string..close(): async Removes all listeners from watched files. Asynchronous, returns Promise. Use with await to ensure bugs don't happen..getWatched(): Returns an object representing all the paths on the file
system being watched by this FSWatcher instance. The object's keys are all the
directories (using absolute paths unless the cwd option was used), and the
values are arrays of the names of the items contained in each directory.Check out third party chokidar-cli, which allows to execute a command on each change, or get a stdio stream of change events.
Sometimes, Chokidar runs out of file handles, causing EMFILE and ENOSP errors:
bash: cannot set terminal process group (-1): Inappropriate ioctl for device bash: no job control in this shellError: watch /home/ ENOSPCThere are two things that can cause it.
fs module used by chokidar: let fs = require('fs'); let grfs = require('graceful-fs'); grfs.gracefulify(fs);echo fs.inotify.max_user_watches=524288 | sudo tee -a /etc/sysctl.conf && sudo sysctl -p.fs.watch
usePolling: true, which will switch backend to resource-intensive fs.watchFileAll fsevents-related issues (WARN optional dep failed, fsevents is not a constructor) are solved by upgrading to v4+.
If you've used globs before and want do replicate the functionality with v4:
// v3
chok.watch('**/*.js');
chok.watch('./directory/**/*');
// v4
chok.watch('.', {
ignored: (path, stats) => stats?.isFile() && !path.endsWith('.js'), // only watch js files
});
chok.watch('./directory');
// other way
import { glob } from 'node:fs/promises';
const watcher = watch(await Array.fromAsync(glob('**/*.js')));
// unwatching
// v3
chok.unwatch('**/*.js');
// v4
chok.unwatch(await Array.fromAsync(glob('**/*.js')));
Why was chokidar named this way? What's the meaning behind it?
Chowkidar is a transliteration of a Hindi word meaning 'watchman, gatekeeper', चौकीदार. This ultimately comes from Sanskrit _ चतुष्क_ (crossway, quadrangle, consisting-of-four). This word is also used in other languages like Urdu as (چوکیدار) which is widely used in Pakistan and India.
MIT (c) Paul Miller (https://paulmillr.com), see LICENSE file.