gulp 单任务多数据源实战:merge-stream 合并独立管线与 globs 数组拼接
【免费下载链接】gulpA toolkit to automate & enhance your workflow项目地址: https://gitcode.com/gh_mirrors/gu/gulp
在同一个 gulp 任务里从多个目录读取文件是构建工作流中的高频需求。本文以 docs/recipes/using-multiple-sources-in-one-task.md 为核心,完整讲解两种官方推荐方案:用
merge-stream将多条独立管线合并为一条,以及用gulp.src的 globs 数组特性结合gulp-concat拼接多个来源。读完你将掌握"一个任务处理多组文件"的两种正确姿势,并能理解其底层流式原理与选择依据。
场景:为什么一个任务需要多个数据源
在真实项目中,一个任务往往不止处理一组文件。以原文档示例中的"静态资源搬运"为例:
bootstrap/js/*.js—— Bootstrap 的 JS 文件,需要拷贝到public/bootstrap/;jquery.cookie/jquery.cookie.js—— jQuery 插件文件,需要拷贝到public/jquery/。
虽然可以直接写两个任务分别处理,但如果两者属于同一个构建阶段(例如都服务于"前端资源"这一目标),把它们放进同一个任务里会让构建逻辑更集中、语义更清晰。这就是"单任务多源"需求的来源。
在 gulp 中,每个gulp.src()都会创建一个独立的读取流,每个.pipe(gulp.dest())又是独立写入流。同一个任务里出现多个src()时,就必须考虑如何把它们组织成一条可被 gulp 正确识别的流。原文档给出了两种官方认可的方案。
方案一:用 merge-stream 合并多条独立管线
这是原文档的第一种做法,适用于每组文件的目标目录不同(甚至每组文件的处理管线都不同)的场景。
// npm install --save-dev gulp merge-stream var gulp = require('gulp'); var merge = require('merge-stream'); gulp.task('test', function() { var bootstrap = gulp.src('bootstrap/js/*.js') .pipe(gulp.dest('public/bootstrap')); var jquery = gulp.src('jquery.cookie/jquery.cookie.js') .pipe(gulp.dest('public/jquery')); return merge(bootstrap, jquery); });拆解这段代码,可以看到清晰的四步:
- 分别创建源流:
gulp.src('bootstrap/js/*.js')与gulp.src('jquery.cookie/jquery.cookie.js')各自按 glob 模式读取文件,产出 Vinyl 对象流; - 分别接上目的地:每条流各自
.pipe(gulp.dest(...)),把文件写到自己的目录。此时bootstrap与jquery都是已经完成"读取→写入"完整链路的独立流对象; - 合并:
merge(bootstrap, jquery)把两条流合并成一条合并流,两条源流中的文件会按添加顺序依次流出; - 返回合并流:
return merge(bootstrap, jquery)是最关键的一步。gulp 的任务系统依靠返回的流来判断任务何时完成(详见 3-creating-tasks.md 与 4-async-completion.md)。如果只返回其中一条流,另一条流的写入可能尚未完成,任务就会提前结束。把merge的结果作为返回值,gulp 会等待两条流全部结束。
注意:
merge-stream是第三方 npm 包(npm install --save-dev gulp merge-stream),不是 gulp 内置模块,但它正是 gulp 生态中处理"多源多目标"的标准配套工具。
方案二:globs 数组 + gulp-concat 拼接单条管线
原文档强调:gulp.src会按 globs 数组中声明的顺序发出文件。利用这一点,可以把多个来源合并进同一条管线,再统一交给下游插件处理。
// npm install gulp gulp-concat var gulp = require('gulp'); var concat = require('gulp-concat'); gulp.task('default', function() { return gulp.src(['foo/*', 'bar/*']) .pipe(concat('result.txt')) .pipe(gulp.dest('build')); });与方案一相比,此方案的关键差异在于:
gulp.src接受数组:查看 src() 的 API 文档,globs参数的类型是string | array。传入['foo/*', 'bar/*']时,gulp 会先匹配foo/*的文件,再匹配bar/*的文件,并按此顺序放入流中;- 共用同一条下游管线:文件依次经过
concat('result.txt')合并成一个文件,再写入build/目录。这正是gulp-concat插件发挥作用的时刻——它读取流中全部文件内容并按到达顺序拼接; - 适用于"多源汇聚成单文件/单目录":当多个来源的文件最终要合并成一个产物(如打包为一个 bundle),或写入同一个目标目录时,这是最简洁的写法。
顺序保证背后的原理:glob base
方案二能保证拼接顺序,与 gulp 的glob base(glob 父路径)机制密切相关。在 API Concepts 中有明确说明:glob base 是 glob 字符串中任意特殊字符之前的那段路径,例如/src/js/**.js的 base 是/src/js/;src()生成的所有 Vinyl 对象都会以该 base 作为base属性,写入时dest()会移除 base 以保留目录结构。
这意味着当你写gulp.src(['foo/*', 'bar/*'])时,foo/*匹配的文件 base 为foo/,bar/*匹配的文件 base 为bar/。虽然它们进入同一条流,但各自的相对目录信息依然保留——这也解释了为什么方案二适合"统一处理后仍保持各自目录结构"或"直接按顺序拼接"的场景。
两种方案如何选择
| 维度 | 方案一:merge-stream | 方案二:globs 数组 |
|---|---|---|
| 目标目录 | 各源流可写往不同目录 | 通常写往同一目录/同一文件 |
| 管线处理 | 各源流可挂载不同的插件链 | 所有源文件共享同一条插件链 |
| 依赖 | 需额外安装merge-stream | 仅需 gulp 本身(拼接需要gulp-concat) |
| 顺序 | 合并流按传入顺序输出各源流 | globs 数组内按声明顺序输出文件 |
| 典型场景 | 多组资源各自拷贝/各自处理 | 多目录源码合并打包成一个 bundle |
选择要点可以概括为:"分流"用 merge-stream,"合流"用 globs 数组。如果各组文件处理方式不同、目的地不同,方案一更合适;如果它们要被统一加工并汇聚成单一产物,方案二更简洁。
源码级原理:src/dest 从何而来
理解了用法之后,看一下当前仓库的实现,能更清楚地把握这两种方案在 gulp 整体架构中的位置。
在 index.js 中可以看到 gulp 实例的核心方法定义:
Gulp.prototype.src = vfs.src; Gulp.prototype.dest = vfs.dest; Gulp.prototype.symlink = vfs.symlink;src()与dest()直接来自 vinyl-fs 这个 Vinyl 适配器模块(当前仓库的 package.json 中声明了"vinyl-fs": "^4.0.2")。也就是说,无论你用方案一还是方案二,底层都是 vinyl-fs 的流在干活——gulp.src负责把文件系统上的真实文件包装成 Vinyl 对象流,gulp.dest负责消费这些 Vinyl 对象并写回文件系统。这解释了为什么两种方案能自由组合:它们操作的是同一套流协议,merge-stream合并的正是这种协议流。
同时,gulp 官方测试 test/index.test.js 通过hasOwnProperty断言确认了src、dest等 API 均挂在 gulp 实例自身,即require('gulp').src是可用的公开接口。
关于 src() 的几个相关选项
若你的"多源"任务涉及更复杂的匹配需求,可以查阅 docs/api/src.md 中src()的完整选项表,其中与多源场景直接相关的包括:
base:显式设置生成 Vinyl 对象的 base,控制输出目录结构;allowEmpty(默认false):当某个 glob 只能匹配单个文件(如jquery.cookie/jquery.cookie.js)且匹配不到时会抛出 "File not found with singular glob" 错误;设为true可抑制;ignore:从匹配结果中排除指定 glob;uniqueBy(默认'path'):去除流中的重复文件。
进阶实践:多源管线的错误处理与复用
为多源管线统一捕获错误
方案一中每条管线都是独立流,错误可能发生在任一流上。官方配方 combining-streams-to-handle-errors.md 指出:默认情况下流上发出error事件若没有监听器会被直接抛出。更稳妥的做法是用stream-combiner2把一长串流合并成单个流,从而只在一处监听error:
var combiner = require('stream-combiner2'); var uglify = require('gulp-uglify'); var gulp = require('gulp'); gulp.task('test', function() { return combiner.obj([ gulp.src('bootstrap/js/*.js'), uglify(), gulp.dest('public/bootstrap') ]) .on('error', console.error.bind(console)); });这个思路同样适用于方案一:可以把merge(bootstrap, jquery)的结果接入统一错误监听,让多源任务失败时错误集中可见。
用流工厂复用多源管线
如果多个任务都包含"多源→统一变换→输出"的相同链路,官方配方 sharing-streams-with-stream-factories.md 提供了lazypipe方案:把共享的插件链封装成工厂函数,不同任务中只需pipe(factory())即可复用,这也天然兼容"多源流各自接同一段工厂管线"的组合方式。此外,incremental-builds-with-concatenate.md 展示了gulp-cached+gulp-remember+gulp-concat配合gulp.watch做增量构建的完整实战,其中同样使用了"多源聚合 + concat"的模式,可作为方案二在 watch 场景下的延伸参考。
小结
- 多源多目标:用
merge-stream合并各条独立的src→dest管线,并务必 return 合并结果,保证 gulp 任务在全部写入完成后才结束; - 多源单目标/单文件:利用
gulp.src支持 globs 数组且按声明顺序发流的特性,配合gulp-concat将多目录文件汇聚成一个产物; - 本质:
src/dest均来自 vinyl-fs,两种方案都建立在统一的 Vinyl 流协议之上,可自由与错误处理、流工厂、增量构建等其他官方配方组合。
更多多源相关实战可继续浏览 docs/recipes/README.md 中的其他配方,如 browserify-with-globs.md、minified-and-non-minified.md 等。
【免费下载链接】gulpA toolkit to automate & enhance your workflow项目地址: https://gitcode.com/gh_mirrors/gu/gulp
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考