简约高效的跨平台文件监视库
为什么选择 Chokidar?
与 2024 年的原始 fs.watch / fs.watchFile 相比,Chokidar 更受青睐的原因有很多:
- 正确报告事件
- macOS 事件会报告文件名
- 不会重复报告事件
- 以添加/更改/取消链接的方式报告更改,而不是无用的重命名
- 使用原子选项,支持原子写入
- 一些文件编辑器会使用它们
- 使用 awaitWriteFinish 选项,支持分块写入
- 大文件通常分块写入
- 支持文件/目录过滤
- 支持符号链接
- 始终支持递归观察,而不是使用原始事件时的部分观察
- 包含限制递归深度的方法
Chokidar 依赖 Node.js 核心 fs 模块,但在使用 fs.watch 和 fs.watchFile 进行观察时,它会对接收到的事件进行规范化处理,通常会通过获取文件统计信息和/或目录内容来检查真实性。基于 fs.watch 的实现是默认设置,可避免轮询并降低 CPU 占用率。需要注意的是,chokidar 会对指定路径范围内的所有内容递归启动监视器,因此要谨慎行事,不要因为监视过多而浪费系统资源。在某些情况下,可以使用 fs.watchFile,它可以利用轮询并占用更多资源。
它于 2012 年为 Brunch 研发,现已用于约 3000 万个存储库,并在生产环境中得到了验证。
2024 年 9 月更新:v4 版本发布!它将依赖关系从 13 个减少到 1 个,删除了对 globs 的支持,增加了对 ESM / Common.js 模块的支持,并将 node.js 的最低版本从 v8 提高到 v14。
开始使用
使用 npm 安装:
npm install chokidar
在代码中使用它:
import chokidar from 'chokidar';
// 使用一行代码来监视当前目录
chokidar.watch('.').on('all', (event, path) => {
console.log(event, path);
});
// 扩展选项
// ----------------
// 初始化监视器。
const watcher = chokidar.watch('file, dir, or array', {
ignored: (path, stats) => stats?.isFile() && !path.endsWith('.js'), // 仅监视 js 文件
persistent: true
});
// 收到事件时使用的东西。
const log = console.log.bind(console);
// 添加事件监视器。
watcher
.on('add', path => log(`已添加文件 ${path}`))
.on('change', path => log(`文件已更改 ${path}`))
.on('unlink', path => log(`删除了文件 ${path}`));
// 更多可能的事件。
watcher
.on('addDir', path => log(`已添加目录 ${path}`))
.on('unlinkDir', path => log(`删除了目录 ${path}`))
.on('error', error => log(`监视器错误:${error}`))
.on('ready', () => log('初始扫描完成。可随时更改'))
.on('raw', (event, path, details) => { // 内部
log('原始事件信息:', event, path, details);
});
// “add”、“addDir ”和 “change ”事件也会以第二种方式接收 stat() 结果
// 参数: 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}`);
});
// 监视新文件。
watcher.add('new-file');
watcher.add(['new-file-2', 'new-file-3']);
// 获取文件系统中被监视的实际路径列表
let watchedPaths = watcher.getWatched();
// 解除对某些文件的监视。
await watcher.unwatch('new-file');
// 停止监视。该方法是异步的!
await watcher.close().then(() => console.log('closed'));
// 完整的选项列表。说明见下文。
// 请勿使用此示例!
chokidar.watch('file', {
persistent: true,
// 忽略 .txt 文件
ignored: (file) => file.endsWith('.txt'),
// 仅监视 .txt 文件
// ignored: (file, _stats) => _stats?.isFile() && !file.endsWith('.txt'),
awaitWriteFinish: true, // 分块写入完成时发出单个事件
atomic: true, // 当使用 “原子写入”(mv _tmp 文件)时,发出适当的事件
// 选项还允许指定以毫秒(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(字符串或字符串数组)。要递归监视的文件、目录的路径。
- options (对象) 下面定义的选项对象:
持久性
- persistent(默认值:true)。表示进程是否应在文件被监视期间继续运行。
路径过滤
- ignored 函数、regex 或路径。定义要忽略的文件/路径。测试的是整个相对路径或绝对路径,而不仅仅是文件名。如果提供的函数有两个参数,则每个路径会被调用两次,一次只有一个参数(路径),第二次有两个参数(路径和该路径的 fs.Stats 对象)。
- ignoreInitial(默认值:false)。如果设置为 false,当 chokidar 发现这些文件路径时(在就绪事件之前),在实例化观看时也会对匹配路径发出 add/addDir 事件。
- followSymlinks(默认值:true)。如果为 false,将只监视符号链接本身的变化,而不是通过链接路径跟踪链接引用和冒泡事件。
- cwd(无默认值)。监视路径的基本目录。与事件一起发出的路径将相对于该目录。
性能
- usePolling(默认值:false)。是使用 fs.watchFile(由轮询支持),还是使用 fs.watch。如果轮询会导致 CPU 占用率过高,请考虑将其设置为 false。通常需要将此设置为 true 才能通过网络成功监视文件,在其他非标准情况下,也可能需要将此设置为 true 才能成功监视文件。在 MacOS 上显式设置为 true 会覆盖 useFsEvents 默认值。您也可以将 CHOKIDAR_USEPOLLING 环境变量设置为 true (1) 或 false (0),以覆盖此选项。
- 轮询特定设置(当 usePolling 为 true 时有效)
- interval (默认值:100)。文件系统轮询的间隔,以毫秒为单位。您也可以设置 CHOKIDAR_INTERVAL 环境变量来覆盖此选项。
- binaryInterval(二进制间隔)(默认值:300)。文件系统轮询二进制文件的间隔。(参见二进制扩展名列表)
- alwaysStat(默认值:false)。如果依赖可能随添加、addDir 和更改事件一起传递的 fs.Stats 对象,请将此设置为 true,以确保即使在底层监视事件尚未提供该对象的情况下也能提供该对象。
- depth(默认值:未定义)。如果设置,将限制遍历子目录的层数。
- awaitWriteFinish(默认值:false)。默认情况下,添加事件将在文件首次出现在磁盘上时触发,此时整个文件尚未写入。此外,在某些情况下,文件写入过程中还会发生一些更改事件。在某些情况下,尤其是监视大文件时,需要等待写入操作完成后再响应文件创建或修改。将 awaitWriteFinish 设置为 true(或一个真值)将轮询文件大小,保持其添加和更改事件,直到大小在可配置的时间内没有变化。适当的持续时间设置在很大程度上取决于操作系统和硬件。为实现精确检测,该参数应相对较高,使文件监视的响应速度大大降低。请谨慎使用。
- options.awaitWriteFinish 可以设置为一个对象,以便调整定时参数:
- awaitWriteFinish.stabilityThreshold(默认值:2000)。文件大小在事件发生前保持不变的时间(毫秒)。
- awaitWriteFinish.pollInterval(默认值:100)。文件大小轮询间隔(毫秒)。
错误
- ignorePermissionErrors(忽略权限错误)(默认值:false)。表示是否尽可能监视没有读取权限的文件。如果由于 EPERM 或 EACCES 导致观察失败,且该选项设置为 true,则错误将被无声压制。
- atomic(默认值:true,如果 useFsEvents 和 usePolling 为 false)。自动过滤使用 “原子写入 ”而非直接写入源文件的编辑器时出现的人工痕迹。如果文件在删除后 100 毫秒内被重新添加,Chokidar 会发出更改事件,而不是先取消链接再添加。如果 100 毫秒的默认值不适合你,你可以通过将原子值设置为自定义值来覆盖它,单位为毫秒。
方法和事件
chokidar.watch() 生成 FSWatcher 实例。FSWatcher 的方法
- .add(path / paths):添加要跟踪的文件和目录。接收字符串数组或仅接收一个字符串。
- .on(event, callback):监听 FS 事件: 监听 FS 事件。可用事件:添加、添加目录、更改、取消链接、取消链接目录、就绪、原始、错误。此外,除 ready、raw 和 error 外,all 也可用,它会在每个事件中发出底层事件名称和路径。
- .unwatch(path / paths):停止监视文件或目录。接受一个字符串数组或仅接受一个字符串。
- .close():异步从监视的文件中移除所有监听器。异步,返回 Promise。与 await 一起使用可确保不会出现错误。
- .getWatched():返回一个对象,表示此 FSWatcher 实例正在监视的文件系统上的所有路径。对象的键是所有目录(使用绝对路径,除非使用了 cwd 选项),值是每个目录中包含的项目名称数组。
CLI
查看第三方 chokidar-cli,它允许在每次更改时执行命令,或获取更改事件的 stdio 流。
故障排除
有时,Chokidar 会耗尽文件句柄,导致 EMFILE 和 ENOSP 错误:
- bash: cannot set terminal process group (-1): Inappropriate ioctl for device bash: no job control in this shell
- Error: watch /home/ ENOSPC
有两种情况可能导致这种情况。
- 通用 fs 操作的文件句柄已耗尽
- 可以通过使用 graceful-fs 来解决,它可以对 chokidar 使用的本地 fs 模块进行猴子补丁:
- 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
- 可以通过使用 graceful-fs 来解决,它可以对 chokidar 使用的本地 fs 模块进行猴子补丁:
- fs.watch的文件句柄已耗尽
- 似乎无法通过 graceful-fs 或操作系统调整来解决
- 可以开始使用 usePolling: true,这将把后端切换为资源密集型的 fs.watchFile
升级到 v4+ 后,所有与 fsevents 相关的问题(WARN 可选 dep 失败、fsevents 不是构造函数)都已解决。
更新日志
- v4(2024 年 9 月):移除 glob 支持和捆绑的 fsevents。将依赖关系从 13 个减少到 1 个。重写类型脚本。将对 node.js 的最低要求提升至 v14+
- v3(2019 年 4 月):CPU 和 RAM 消耗大幅改善;依赖项/软件包大小减少了 17 倍,Node.js 要求提升至 v8.16+。
- v2(2017 年 12 月):globs 现在仅支持 posix-style。修复了大量错误。
- v1(2015 年 4 月):支持 glob、支持 symlink,并修复了大量错误。支持 Node 0.8+
- v0.1(2012 年 4 月): 首次发布,从 Brunch 中提取
升级
如果您以前使用过 globs,并希望在 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 glob('**/*.js'));
还有
为什么 Chowkidar 会有这样的名字?其背后的含义是什么?
Chowkidar 是一个印地语单词चौकीदार的音译,意思是 “看门人、守门人”。这个词最终来源于梵文चतुष्क(十字路口,四边形,由四个组成)。这个词在其他语言中也有使用,如在巴基斯坦和印度广泛使用的乌尔都语(چوکیدار)。
许可证
MIT (c) Paul Miller (https://paulmillr.com),参见 LICENSE 文件。
发表回复