Gulp watch() 完全指南:用文件监听器自动化你的工作流
【免费下载链接】gulpA toolkit to automate & enhance your workflow项目地址: https://gitcode.com/gh_mirrors/gu/gulp
watch()是 Gulp 中用于监听文件系统变化并自动触发任务的核心 API。它把 glob 模式与 任务连接起来:当匹配的文件被创建、修改或删除时,任务被自动执行。读完本文,你将掌握watch()的完整配置项、队列与延迟机制、如何规避同步任务陷阱,以及如何直接操作底层 chokidar 实例实现细粒度控制。
watch() 的工作原理
watch()API 通过一个文件系统监听器将 globs 连接到 tasks。它监听与 globs 匹配的文件变化,并在变化发生时执行对应的任务。如果任务没有发出 异步完成信号,那么它永远不会被第二次执行。
该 API 基于最常见的用例提供了内置的延迟(delay)和排队(queueing)机制。
const { watch, series } = require('gulp'); function clean(cb) { // body omitted cb(); } function javascript(cb) { // body omitted cb(); } function css(cb) { // body omitted cb(); } exports.default = function() { // You can use a single task watch('src/*.css', css); // Or a composed task watch('src/*.js', series(clean, javascript)); };在上面的例子中,watch()的第一个参数是 glob 字符串,第二个参数可以是一个任务函数,也可以是由series()或parallel()生成的组合任务。当src/*.css下任一文件变化时,css任务会被执行;当src/*.js下任一文件变化时,clean和javascript会按顺序依次执行。
从源码看,index.js 中Gulp.prototype.watch的实现会对任务做一次this.parallel(task)包装,因此传入watch()的任务会被统一纳入 Gulp 的任务系统,与通过gulp.task()注册的任务走同一套异步完成判定逻辑。这也解释了为什么传给watch()的任务必须遵守异步完成约定。
签名与参数
更完整的签名定义参见 watch() API 参考:
watch(globs, [options], [task])| 参数 | 类型 | 说明 |
|---|---|---|
| globs(必填) | string / array | 要在文件系统上监听的 glob 模式,可以是单个字符串或数组(数组中可混入!开头的负向 glob 用于排除) |
| options | object | 详见下文 完整配置项 |
| task | function / string | 任务函数或由series()、parallel()生成的组合任务 |
当globs传入非字符串或数组中包含非字符串时,watch()会抛出错误,错误信息为"Non-string provided as watch path"。当task传入字符串或数组时,抛出错误"watch task has to be a function (optionally generated by using gulp.parallel or gulp.series)"——这一点在 index.js 中通过显式类型检查实现,test/watch.js 中也用两个测试用例验证了传字符串和数组都会抛出该错误。
警告:避免同步任务
与注册进任务系统的任务一样,监听器的任务不能是同步的。如果传入同步任务,其完成状态无法被判定,任务将不会被再次执行——因为它被假定为仍在运行。
这里不会提供任何错误或警告信息,因为文件监听器会让你的 Node 进程一直保持运行。由于进程不会退出,就无法判断任务到底是执行完了,还是只是运行了很长、很长的时间。
这正是 "Did you forget to signal async completion?" 问题的变体。关于如何正确发出完成信号(返回 stream、promise、event emitter、child process、observable,或使用 error-first callback),请阅读 异步完成。最简单的规避方式是:任务内部什么都不返回时,务必接收cb参数并在异步操作结束后调用cb()。
被监听的事件
默认情况下,监听器在文件被创建、修改或删除时执行任务,即默认监听'add'、'change'、'unlink'三个事件。
如果需要监听不同的事件,可以在调用watch()时使用events选项。可用事件包括'add'、'addDir'、'change'、'unlink'、'unlinkDir'、'ready'、'error'。此外还有'all',它代表除'ready'和'error'之外的所有事件。
const { watch } = require('gulp'); exports.default = function() { // All events will be watched watch('src/*.js', { events: 'all' }, function(cb) { // body omitted cb(); }); };events选项会被直接透传给底层的 chokidar 监听器。事件语义上,'add'是文件被创建,'addDir'是目录被创建,'change'是文件内容变化,'unlink'是文件被删除,'unlinkDir'是目录被删除。
初始执行
调用watch()后,任务并不会立即执行,而是等待第一次文件变化。
如果希望在第一次文件变化之前就执行任务,可将ignoreInitial选项设置为false:
const { watch } = require('gulp'); exports.default = function() { // The task will be executed upon startup watch('src/*.js', { ignoreInitial: false }, function(cb) { // body omitted cb(); }); };这一选项同样透传给 chokidar,但 Gulp 将其默认值从 chokidar 的false改成了true。这一行为差异在 watch() API 参考 的选项表格中有明确标注。典型使用场景是启动时先做一次完整构建,再进入增量监听模式。值得注意的是 test/watch.js 中的测试验证了:默认情况下(ignoreInitial为true),仅仅创建文件而不再修改,任务不会被触发。
队列机制(Queueing)
每个watch()都会保证:当前正在运行的任务不会再次并发执行。当任务运行期间发生文件变化时,会有一个执行排队,等待当前任务结束后再运行。同一时刻只能有一个执行在排队。
const { watch } = require('gulp'); exports.default = function() { // The task will be run (concurrently) for every change made watch('src/*.js', { queue: false }, function(cb) { // body omitted cb(); }); };要禁用排队,将queue选项设置为false,此时每一次变化都会(并发地)触发一次任务执行。
队列机制对长时间运行的任务(如构建、压缩、上传)尤其重要,它避免了文件批量变化时任务互相重叠导致资源竞争或输出错乱。测试用例 test/watch.js 也展示了由gulp.series('task1', 'task2')组成的组合任务在一次触发中会按顺序完整执行。
延迟机制(Delay)
文件变化后,监听任务不会立即运行,而是要等 200ms 的延迟过去。这是为了避免在很多文件同时变化时过早启动任务——比如一次"查找并替换"操作会瞬间触发大量 change 事件。
const { watch } = require('gulp'); exports.default = function() { // The task won't be run until 500ms have elapsed since the first change watch('src/*.js', { delay: 500 }, function(cb) { // body omitted cb(); }); };要调整延迟时长,将delay选项设置为一个正整数。默认值 200ms 在大多数场景下已经是合理的"防抖"窗口:它会在首个变化事件到达后重置计时,只有在一段时间内没有新变化时任务才会真正启动。
完整配置项(options)
watch()支持完整的配置选项,其中绝大多数会原样透传给底层 chokidar。以下表格摘自 watch() API 参考,覆盖全部选项及其默认值:
| 名称 | 类型 | 默认值 | 说明 |
|---|---|---|---|
| ignoreInitial | boolean | true | 若为false,任务会在实例化过程中、文件路径被发现时立即调用。用于启动时触发任务。**注意:**该选项透传给 chokidar,但默认值被 Gulp 改为true(chokidar 默认为false) |
| delay | number | 200 | 文件变化与任务执行之间的毫秒延迟。允许在大量变化时等待任务执行,例如对许多文件做查找替换 |
| queue | boolean | true | 为true且任务正在运行时,文件变化只会排队一次任务执行。防止长时间任务相互重叠 |
| events | string / array | ['add', 'change', 'unlink'] | 触发任务执行的事件。可以是'add'、'addDir'、'change'、'unlink'、'unlinkDir'、'ready'和/或'error'。另外'all'代表除'ready'与'error'外的所有事件。直接透传给 chokidar |
| persistent | boolean | true | 若为false,监听器将不会让 Node 进程保持运行。不建议禁用。直接透传给 chokidar |
| ignored | array / string / RegExp / function | 定义要忽略的 globs。若提供函数,每个路径会被调用两次——一次只传路径,一次传路径及该文件的fs.Stats对象。直接透传给 chokidar | |
| followSymlinks | boolean | true | 为true时,符号链接本身和链接目标文件的变化都会触发事件;为false时,只有符号链接本身的变化触发事件。直接透传给 chokidar |
| cwd | string | 将与任何相对路径拼接形成绝对路径的目录。绝对路径会忽略该选项。用它来避免将 globs 与path.join()混用。直接透传给 chokidar | |
| disableGlobbing | boolean | false | 若为true,所有 globs 都被当作字面路径名处理,即使含有特殊字符。直接透传给 chokidar |
| usePolling | boolean | false | 为false时使用fs.watch()(Mac 上使用 fsevents)监听;为true时改用fs.watchFile()轮询——在网络上或其他非标准场景下监听文件时需要。会覆盖useFsEvents的默认行为。直接透传给 chokidar |
| interval | number | 100 | 与usePolling: true配合使用。文件系统轮询的间隔。直接透传给 chokidar |
| binaryInterval | number | 300 | 与usePolling: true配合使用。对二进制文件进行文件系统轮询的间隔。直接透传给 chokidar |
| useFsEvents | boolean | true | 为true时,若可用则使用 fsevents 监听;若显式设为true,将取代usePolling选项;若设为false,会自动把usePolling设为true。直接透传给 chokidar |
| alwaysStat | boolean | false | 为true时始终对变化的文件调用fs.stat()——会拖慢文件监听器。fs.Stat对象只有在直接使用 chokidar 实例时才可用。直接透传给 chokidar |
| depth | number | 表示要监听的目录嵌套层级数。直接透传给 chokidar | |
| awaitWriteFinish | boolean | false | 不要使用该选项,改用delay。直接透传给 chokidar |
| ignorePermissionErrors | boolean | false | 设为true可监听没有读权限的文件;若因 EPERM 或 EACCES 错误导致监听失败,会被静默跳过。直接透传给 chokidar |
| atomic | number | 100 | 仅在useFsEvents与usePolling均为false时生效。自动过滤某些编辑器"原子写入"产生的中间产物。若某文件在被删除后指定毫秒内被重新添加,将发出 change 事件而非 unlink 加 add 事件。直接透传给 chokidar |
其中几个选项在实际工程中尤其值得注意:
cwd:在 test/watch.js 的测试中,gulp.watch('watch-func.txt', { cwd: outpath }, ...)用cwd指定基准目录,从而可以用简洁的相对路径进行监听。ignored:测试用例 test/watch.js 展示了['*', '!ignored.txt']这种"全监听 + 负向排除"的组合,被忽略文件的变化不会触发任务。usePolling+interval/binaryInterval:在网络共享目录、虚拟机挂载盘或某些容器环境中,原生文件事件可能不可靠,此时应开启轮询模式。
使用监听器实例(chokidar instance)
你可能用不到这个特性,但如果你需要完全掌控变化文件——比如访问路径或元数据——可以使用watch()返回的 chokidar 实例。
请务必注意:返回的 chokidar 实例不具备排队、延迟或异步完成功能。也就是说,只有在你确实需要注册细粒度事件处理器时,才应该绕开任务系统直接操作它。
const { watch } = require('gulp'); const watcher = watch(['input/*.js']); watcher.on('change', function(path, stats) { console.log(`File ${path} was changed`); }); watcher.on('add', function(path, stats) { console.log(`File ${path} was added`); }); watcher.on('unlink', function(path, stats) { console.log(`File ${path} was removed`); }); watcher.close();watcher 实例方法
watch()返回的实例暴露了以下方法(详细说明见 watch() API 参考 的 "Chokidar instance" 章节):
watcher.on(eventName, eventHandler)
注册当指定事件发生时被调用的事件处理函数。事件名可选'add'、'addDir'、'change'、'unlink'、'unlinkDir'、'ready'、'error'或'all'。
事件处理函数的参数:
| 参数 | 类型 | 说明 |
|---|---|---|
| path | string | 发生变化的文件路径。若设置了cwd选项,路径会移除cwd前缀变成相对路径 |
| stats | object | 一个fs.Stat对象,但也可能是undefined。若alwaysStat设为true,stats始终会被提供 |
watcher.close()
关闭文件监听器。关闭后不再发出任何事件。
watcher.add(globs)
向一个已在运行的监听器实例添加额外的 globs。参数globs为 string 或 array,即要额外监听的 glob 模式。
watcher.unwatch(globs)
移除正在被监听的 globs,监听器继续保留剩余路径。参数globs为 string 或 array,即要移除的 glob 模式。
在 test/watch.js 中还有一个值得注意的用法:当不传任务回调、只传 options 时,options 不会被丢弃,你可以通过.on('change', ...)注册自己的处理器,并且处理器拿到的filepath是相对于cwd的路径(测试中用path.resolve(cwd, filepath)与绝对路径做了比对验证)。
可选依赖:fsevents
Gulp 有一个可选依赖叫 fsevents,它是 Mac 专用的文件监听器。如果你看到 fsevents 的安装警告——"npm WARN optional SKIPPING OPTIONAL DEPENDENCY: fsevents"——这不是问题。
如果 fsevents 的安装被跳过,会使用备用的监听器;此时你的 gulpfile 中出现的任何错误都与该警告无关。在 package.json 中,Gulp 直接依赖glob-watcher(^6.0.0),而 fsevents 正是由这条依赖链按平台条件安装的可选依赖——非 macOS 平台(Linux、Windows)出现该警告完全正常,可以放心忽略。
从源码验证 watch() 的完整调用链
从当前仓库源码可以梳理出watch()的完整实现链路:
- index.js 中定义
Gulp.prototype.watch:先做参数类型校验(task必须是函数),若第二个参数是函数则把它当作任务并把 options 置为空对象,最后将任务经this.parallel(task)包装后委托给glob-watcher包; - index.js 在
Gulp构造函数中将watch等 APIbind到实例上,从而支持解构导入:const { watch } = require('gulp')或 ESM 的import { watch } from 'gulp'(见 index.mjs); glob-watcher内部再基于 chokidar 构建监听器,并实现 200ms 延迟、单次排队以及任务系统的异步完成接入;- 队列与延迟行为由
glob-watcher提供,而透传给 chokidar 的选项(events、ignored、usePolling、cwd等)则保持原生语义。
这也解释了文档中反复强调的三点事实:任务必须是异步的(异步完成判定来自任务系统)、队列与延迟只在通过任务方式使用时生效(直接操作 chokidar 实例时两者均不可用)、默认情况下启动时不执行任务(ignoreInitial被 Gulp 改为true)。
常见使用模式小结
把以上机制组合起来,一个健壮的 watch 任务通常长这样:
const { watch, series, src, dest } = require('gulp'); function clean(cb) { // 清空构建目录 cb(); } function scripts() { return src('src/**/*.js') .pipe(dest('dist')); } exports.default = function() { // 启动时先构建一次,之后监听变化并排队串行执行 watch('src/**/*.js', { ignoreInitial: false }, series(clean, scripts)); // 忽略临时目录,避免无限循环 watch('src/**/*.scss', { ignored: ['dist/**', 'tmp/**'] }, series(clean, styles)); };需要记住的关键点:
- 给
watch()的任务一定要通过返回 stream/promise 或调用cb()发出异步完成信号; - 默认 200ms 延迟 + 单次排队已经覆盖大多数场景,一般无需修改;
- 文件批量变动(如查找替换、git 切换分支)时,队列与延迟能有效防止任务风暴;
- 网络盘、容器等文件事件不可靠的环境,考虑
usePolling: true; - 需要获取变化文件的路径、统计信息或动态增删监听路径时,再直接使用返回的 chokidar 实例。
进一步阅读:watch() API 参考、Globs 详解、创建任务、异步完成。
【免费下载链接】gulpA toolkit to automate & enhance your workflow项目地址: https://gitcode.com/gh_mirrors/gu/gulp
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考