Gulp 5 Glob 匹配完全指南:从*、**到!的路径模式语法与工程实践
【免费下载链接】gulpA toolkit to automate & enhance your workflow项目地址: https://gitcode.com/gh_mirrors/gu/gulp
本文是 gulp 官方 Getting Started 系列中 Explaining Globs 的深度扩展,系统讲解 glob 的语法要素(段与分隔符、*、**、!)、src()对 glob 的消费规则,以及 v5 中负向 glob、有序 glob、重叠 glob 的行为变迁。读完本文,你将能写出准确、高效、跨平台可用的 gulp 文件匹配模式,并理解其背后的底层实现与测试证据。
glob 是什么:定位文件的语言
glob 是由字面量字符和通配符字符组成的字符串,用于匹配文件路径。而 globbing 则是使用一个或多个 glob 在文件系统上定位文件的行为。在 gulp 中,glob 是构建管道的起点——你用它告诉 gulp"把哪些文件交给任务处理"。
gulp 的src()方法接受单个 glob 字符串或glob 数组,来决定你的管道将操作哪些文件:
const { src, dest } = require('gulp'); // 单个 glob src('src/*.js') // glob 数组 src(['src/**/*.js', '!src/vendor/**'])两个关键约束需要牢记:
- 至少必须有一个匹配。如果所有 glob 都没有匹配到任何文件,
src()会报错(详见下文"错误行为"小节,可用allowEmpty选项抑制)。 - 负向 glob 作用于全部正向 glob。当使用 glob 数组时,任何以
!开头的负向 glob 都会从任何正向 glob 的匹配结果中移除文件——这是 gulp v5 引入的新语义,v5 之前的行为有所不同(详见下文"负向 glob"小节)。
gulp 中所有接受路径的地方几乎都支持 glob,包括src()、watch()(见 Watching Files)以及ignore选项等,因此掌握 glob 语法是高效使用 gulp 的前提。
段与分隔符:跨平台的一致性约定
理解 glob 的第一步是掌握**段(segment)与分隔符(separator)**的概念:
- 段:两个分隔符之间的内容。例如
scripts/nested/index.js由scripts、nested、index.js三段组成。 - 分隔符:在 glob 中永远是
/字符,与操作系统无关——即使在路径分隔符为\的 Windows 上也不例外。
在 glob 中,\被保留用作转义字符(escape character),而不是路径分隔符。下面的示例中*被转义,因此被当作字面量字符而非通配符:
'glob_with_uncommon_\\*_character.js'注:这是 JavaScript 源码中的写法,
\\表示一个真正的反斜杠,最终传给src()的 glob 字符串为glob_with_uncommon_\*_character.js,其中\*意味着"匹配字面量*字符"。
因此,避免使用 Node 的path方法来创建 glob,例如path.join。在 Windows 上,Node 会使用\作为路径分隔符,这样生成的"glob"是无效的:
const invalidGlob = path.join(__dirname, 'src/*.js'); // ❌ 在 Windows 上会产生无效 glob同理,也要避免使用__dirname全局变量、__filename全局变量或process.cwd()来拼接 glob,原因相同。这正是src()提供cwd、base等选项的原因——详见 src() API 参考 中的说明,用选项来指定工作目录,而不是用字符串拼接。
特殊字符*(单星):匹配单个段内的任意字符
*匹配单个段内任意数量的字符——包括零个字符。它适合匹配同一个目录内的文件。
'*.js'这个 glob 会匹配index.js这类文件,但不会匹配:
scripts/index.js(跨了段)scripts/nested/index.js(跨了多段)
因为它只在单一段内生效。这是 glob 与正则表达式的一个重要区别:*在 glob 中永远不会跨越/分隔符。
特殊字符**(双星):跨段匹配
**匹配跨多个段的任意数量字符——同样包括零个。它适合匹配嵌套目录中的文件。
'scripts/**/*.js'这个 glob 会匹配:
scripts/index.jsscripts/nested/index.jsscripts/nested/twice/index.js
务必注意限制双星 glob 的范围。在上面的示例中,scripts/前缀是刻意保留的:如果不加这个前缀,**/*.js会匹配当前工作目录树下所有.js文件,包括node_modules等大型目录中的文件,导致匹配范围失控、性能下降。
仓库测试 test/src.js 直接验证了双星 glob 的行为:
// 深层次 glob:匹配嵌套目录下的 .jade 文件 var stream = gulp.src('./fixtures/**/*.jade', { cwd: __dirname }); // 期望匹配 ./fixtures/test/run.jade // 更深的 glob:./fixtures/**/*.dmc 期望匹配 2 个文件 var stream = gulp.src('./fixtures/**/*.dmc', { cwd: __dirname }); // end 事件时断言 a === 2测试中的cwd选项指向__dirname,正是为了在不依赖 glob 字符串拼接的前提下指定基准目录。
特殊字符!(负向):从结果中排除文件
以!字符开头的 glob 会"否定"该 glob,将其匹配的文件完全排除。负向 glob 是所有 glob 语法中最实用的部分,常用于"匹配全部,但排除某些目录"的场景。
['scripts/**/*.js', '!scripts/vendor/**']上面的数组会遍历scripts/目录下所有以.js结尾的文件,但排除scripts/vendor/目录下的所有文件。
v5 行为变化:在 gulp v5 之前,负向 glob 只对其在数组中的位置之后的 glob 生效;而在 v5 中,所有负向 glob 都会应用于每一个正向 glob。这意味着无论负向 glob 在数组中的位置如何,它都会生效,行为更符合直觉,也与生态中大多数 globbing 库保持一致。
负向 glob 也可以作为限制双星 glob的替代方案——与其在正向 glob 中层层限定前缀,不如"先全选、再排除":
['**/*.js', '!node_modules/**']这个模式等效于"匹配所有 JS 文件,但排除node_modules目录",在大型项目中比硬编码前缀更稳健。
仓库测试 test/src.js 同样覆盖了负向 glob 场景:
var globArray = [ './fixtures/stuff/*.dmc', '!fixtures/stuff/test.dmc', ]; // 最终只匹配到 ./fixtures/stuff/run.dmc 一个文件错误行为与allowEmpty
当globs只能匹配单个文件(如foo/bar.js这种不含通配符的路径)且未找到匹配时,src()会抛出 "File not found with singular glob" 错误;要抑制该错误,可将allowEmpty选项设为true。当传入无效的 glob 时,会抛出 "Invalid glob argument" 错误。详见 src() API 参考。
有序 glob:v5 的移除与替代方案
v5 之前的 gulp 版本允许"有序 glob"(ordered globs)——即多个 glob 按给定顺序匹配文件,控制输出顺序。这一特性已在 v5 中移除,以对齐生态中大多数 globbing 库的语义。
如果你确实需要"有序 glob"的功能(例如按特定顺序合并第三方库文件),可以使用ordered-read-streams库来合并多个流:
const order = require("ordered-read-streams"); exports.default = function () { return order([ gulp.src("input/jquery/dist/jquery.js"), gulp.src("input/detect_swipe/jquery.detect_swipe.js"), ]).pipe(gulp.dest('output/')); }这个方案将每个src()产生的流按顺序组合,再统一输出到目标目录,保留了输出顺序的可控性。
重叠 glob 与去重行为
两个或多个 glob 如果(有意或无意)匹配到同一个文件,即构成重叠(overlapping)。例如['**/*.js', 'src/*.js']中,src/index.js会被两个 glob 同时匹配。
重叠时的行为规则:
- 在同一个
src()调用内:gulp 会尽力去除重复文件,默认按path属性去重(可通过uniqueBy选项自定义去重键,详见 src() API 参考)。 - 跨多个
src()调用:gulp 不会跨调用去重。如果两个不同的src()匹配到同一文件,该文件会以两个 Vinyl 对象的形式进入管道。
了解这一点有助于避免意外的重复输出——例如在管道中间再次调用src()追加文件时,如果 glob 重叠,文件会被再次加入(详见 Working with Files 中的"Adding files to the stream")。
源码视角:globs 如何驱动 gulp 的核心 API
从源码结构看,gulp 本体(index.js)非常薄,它把文件系统的读写能力委托给了vinyl-fs模块:
// index.js Gulp.prototype.src = vfs.src; Gulp.prototype.dest = vfs.dest; Gulp.prototype.symlink = vfs.symlink;也就是说,你调用的gulp.src(globs, [options])实际是 vinyl-fs 提供的适配器,它负责:把 glob 交给底层匹配库去定位文件、按匹配结果创建 Vinyl 文件对象(path、contents、base等属性)、并以 Node 流的形式输出。globs 是这一切的输入契约。
在 package.json 中可以确认当前仓库对应 gulp 5.0.1 版本,其核心依赖包括vinyl-fs、glob-watcher、undertaker、gulp-cli等——每个模块各司其职(可参考 API Concepts 中的模块列表)。
glob 不仅驱动src(),也驱动watch():watch(globs, [options], [task])使用文件系统监听器监听匹配 globs 的文件变化并触发任务执行(详见 watch() API 参考 与 Watching Files)。因此一套 glob 语法知识,同时覆盖了"读文件"和"盯文件"两大场景。
另外,src()中与 glob 强相关的选项也值得掌握(完整列表见 src() API 参考):
| 选项 | 默认值 | 与 glob 的关系 |
|---|---|---|
cwd | process.cwd() | 与相对路径拼接形成绝对路径,用于避免用path.join()拼 glob |
base | 由 glob 推断 | 显式设置 Vinyl 对象的base属性(即 glob base,见 API Concepts) |
allowEmpty | false | 为true时抑制单个文件 glob 找不到匹配时的报错 |
uniqueBy | 'path' | 按指定属性或函数结果去除流中的重复文件 |
dot | false | 为true时 glob 可匹配.gitignore这类点文件 |
noglobstar | false | 为true时把**当作*处理 |
nocase | false | 为true时执行大小写不敏感匹配 |
matchBase | false | 为true时,不含/的 glob(如*.js)等效于**/*.js |
ignore | — | 附加的排除 glob,与负向 glob 合并生效 |
这些选项大多直接透传给底层的 globbing 库(如 glob-stream、anymatch),理解它们能帮你写出更精确、更高效的匹配规则。
进阶学习资源
本文覆盖了 gulp 使用中绝大多数 glob 场景。若需要更深入的钻研,社区中以下资料值得参考(均可通过 npm/GitHub 搜索找到):
- micromatch文档:gulp 底层依赖的高性能 glob 匹配库,涵盖完整的语法细节与选项。
- node-glob 的 Glob Primer:最经典的 glob 语法入门读物,详细解释了
*、**、?、字符类等所有通配符语义。 - Begin 的 Globbing 文档:从"什么是 globbing"讲起的简明教程。
- Wikipedia 的 Glob 词条:了解 glob 的历史渊源与各语言实现差异。
回到仓库本身,Explaining Globs 是官方 Getting Started 系列(见 getting-started/README)的第 6 篇,建议按顺序与 Working with Files 搭配阅读,前者讲透匹配语法,后者讲透文件在管道中的流转方式;再结合 test/src.js 中的测试用例动手验证,即可彻底掌握 gulp 的文件匹配体系。
【免费下载链接】gulpA toolkit to automate & enhance your workflow项目地址: https://gitcode.com/gh_mirrors/gu/gulp
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考