gulp 从内存 Buffer 构建流:绕过 gulp.src() 的 Vinyl 流创建实战
【免费下载链接】gulpA toolkit to automate & enhance your workflow项目地址: https://gitcode.com/gh_mirrors/gu/gulp
导读
在 gulp 中,几乎每个任务都以gulp.src()读取磁盘文件作为起点,但现实中的构建场景并非总是如此——你可能需要把一段已经存在于内存变量中的内容(例如拼接好的代码、模板渲染结果、版本号文件内容)直接包装成一个可继续pipe()的 gulp 流,而根本不触碰文件系统。本文基于仓库中的 make-stream-from-buffer.md 配方,系统讲解“从内存内容创建流”的完整方案:先分析为什么不能用gulp.src()直接完成这类任务,再给出一个可运行的“按版本拼接 lib 文件”完整示例,深入剖析vinyl-source-stream、vinyl-buffer、gulp-tap、event-stream在其中的分工,最后结合仓库源码说明 gulp 的 Vinyl 抽象、任务编排与监听机制,帮助你掌握这种无需磁盘中转的数据流构建技巧。
场景:内容在变量里,不在磁盘上
gulp.src()的作用是“从文件系统读取 Vinyl 对象” —— 这是 gulp 默认的流起点。但在下面这类需求中,它并不适用:
有一个目录存放若干 JS 库文件,另一个目录存放某个模块的多个版本文件。构建目标是:为每个版本生成一个 JS 文件,内容为“所有库文件拼接结果 + 该版本文件内容”。
按逻辑拆解,构建步骤为:
- 加载 lib 文件;
- 拼接 lib 文件内容;
- 加载版本文件;
- 对每个版本文件,把 libs 拼接结果与版本内容再拼接;
- 对每个版本文件,把最终结果输出成一个文件。
假设源文件结构如下:
├── libs │ ├── lib1.js │ └── lib2.js └── versions ├── version.1.js └── version.2.js期望的输出是:
└── output ├── version.1.complete.js # lib1.js + lib2.js + version.1.js └── version.2.complete.js # lib1.js + lib2.js + version.2.js问题在于:第 4、5 步的数据完全存在于内存变量中(拼接后的字符串),它没有对应的物理文件路径。若先写临时文件再gulp.src()读取,既低效又增加出错面。因此需要一种“凭空造出一个 gulp 流”的手段——这正是本配方要解决的核心问题。
核心思路:把字符串写入一个全新的流
gulp 的流是 Node 的可写/可读流,管道中的每个文件都是一个Vinyl 对象(虚拟文件:包含path、contents、stat等元数据)。因此“从内存造流”的本质是三步:
- 用一个假文件名(
vinyl-source-stream)创建流,它会把后续write()进来的字符串/ Buffer 包装成带path的 Vinyl 对象; - 将字符串内容
write()进该流; - 把流接进标准管道:
vinyl-buffer()将内容规整为 Buffer 形式,再交给gulp.dest()落盘。
对应地,任务中还需要两个配套工具:gulp-tap用于“偷看”流中每个文件的contents并缓存到内存;event-stream用于合并多个流的结束事件,避免任务提前完成。
完整示例代码
var gulp = require('gulp'); var source = require('vinyl-source-stream'); var vinylBuffer = require('vinyl-buffer'); var tap = require('gulp-tap'); var concat = require('gulp-concat'); var size = require('gulp-size'); var path = require('path'); var es = require('event-stream'); var memory = {}; // we'll keep our assets in memory // task of loading the files' contents in memory gulp.task('load-lib-files', function() { // read the lib files from the disk return gulp.src('src/libs/*.js') // concatenate all lib files into one .pipe(concat('libs.concat.js')) // tap into the stream to get each file's data .pipe(tap(function(file) { // save the file contents in memory memory[path.basename(file.path)] = file.contents.toString(); })); }); gulp.task('load-versions', function() { memory.versions = {}; // read the version files from the disk return gulp.src('src/versions/version.*.js') // tap into the stream to get each file's data .pipe( tap(function(file) { // save the file contents in the assets memory.versions[path.basename(file.path)] = file.contents.toString(); })); }); gulp.task('write-versions', function() { // we store all the different version file names in an array var availableVersions = Object.keys(memory.versions); // we make an array to store all the stream promises var streams = []; availableVersions.forEach(function(v) { // make a new stream with fake file name var stream = source('final.' + v); var streamEnd = stream; // we load the data from the concatenated libs var fileContents = memory['libs.concat.js'] + // we add the version's data '\n' + memory.versions[v]; // write the file contents to the stream stream.write(fileContents); process.nextTick(function() { // in the next process cycle, end the stream stream.end(); }); streamEnd = streamEnd // transform the raw data into the stream, into a vinyl object/file .pipe(vinylBuffer()) //.pipe(tap(function(file) { /* do something with the file contents here */ })) .pipe(gulp.dest('output')); // add the end of the stream, otherwise the task would finish before all the processing // is done streams.push(streamEnd); }); return es.merge.apply(this, streams); }); //============================================ our main task gulp.task('default', gulp.series( // load the files in parallel gulp.parallel('load-lib-files', 'load-versions'), // ready to write once all resources are in memory 'write-versions' ) ); //============================================ our watcher task // only watch after having run 'default' once so that all resources // are already in memory gulp.task('watch', gulp.series( 'default', function() { gulp.watch('./src/libs/*.js', gulp.series( 'load-lib-files', 'write-versions' )); gulp.watch('./src/versions/*.js', gulp.series( 'load-lib-files', 'write-versions' )); } ));关键点逐段拆解
内存缓存:memory对象与gulp-tap
load-lib-files与load-versions两个任务负责把磁盘内容搬进内存对象:
gulp.src('src/libs/*.js')读入库文件;concat('libs.concat.js')把它们合并为一个名为libs.concat.js的虚拟文件(内容仍是流中的 Vinyl 对象,并未落盘);tap()在流经过时回调每个file,用file.contents.toString()取出 Buffer 内容,并以path.basename(file.path)为键存入memory。
这里memory是一个普通的模块级对象,跨任务共享——这是“先把资源加载到内存,再集中使用”这一策略的载体。注意file.contents此时是 Buffer,可直接调用toString()(参见 docs/api/vinyl.md 中对contents属性“ReadableStream / Buffer / null”的说明;若内容为流,则需先缓冲才能同步读取)。
凭空造流:vinyl-source-stream
write-versions任务的核心是下面这段:
var stream = source('final.' + v); // 假文件名 var fileContents = memory['libs.concat.js'] + '\n' + memory.versions[v]; stream.write(fileContents); // 把字符串写入流 process.nextTick(function() { stream.end(); // 下一轮事件循环结束流 }); streamEnd = streamEnd .pipe(vinylBuffer()) .pipe(gulp.dest('output'));要点:
source('final.' + v)创建一个流,同时给它一个“假文件名”(如final.version.1.js)。vinyl-source-stream负责把写入的原始数据(字符串或 Buffer)转换成带有path的 Vinyl 对象——这是它替代gulp.src()的位置。stream.write(fileContents)把拼接好的内容写入流。由于写入方(write-versions任务)与消费方(下游管道)在同一同步代码段内,需要在下一个事件循环周期再stream.end(),即用process.nextTick包裹,保证流能先处理已写入的数据,这是该配方中容易踩坑的关键细节。.pipe(vinylBuffer())把流式内容转换为 Buffer 形态的 Vinyl 对象(contents为 Buffer),确保后续gulp.dest()能正常落盘;示例中还注释了一行tap(),提示你可以在落盘前对最终文件内容做二次处理(如压缩、注入时间戳等)。.pipe(gulp.dest('output'))是流的终点,把每个内存构造出的 Vinyl 对象写到output目录,文件名取流创建时给定的假名(final.version.1.js等)。dest()会依据 Vinyl 对象的base/path计算输出路径,详见 docs/api/dest.md。
异步完成:为什么必须es.merge多个流
write-versions需要为每个版本生成一个独立流,然后返回合并结果给 gulp 作为任务完成信号:
streams.push(streamEnd); // ... return es.merge.apply(this, streams);gulp 任务通过“返回值”来判定是否完成——返回流、Promise、EventEmitter、child process 或 observable 均可(见 docs/getting-started/4-async-completion.md)。这里没有返回单个流,而是多个流,因此必须用event-stream的merge把所有流的结束事件合并成一个,否则任务会在各流尚未写完时就提前结束,导致输出文件不完整。注释里也明确写到:add the end of the stream, otherwise the task would finish before all the processing is done。
任务编排:series / parallel 的正确姿势
gulp.task('default', gulp.series( gulp.parallel('load-lib-files', 'load-versions'), 'write-versions' ));- 两个“加载”任务互不依赖,用
gulp.parallel并行执行,加速资源准备; write-versions依赖内存中的全部资源,必须放在series的第二个位置串行执行;gulp.series/gulp.parallel是本仓库 index.js 中从undertaker继承的任务编排能力(Gulp.prototype上绑定了series、parallel、task、watch等方法),遵循 error-first 完成约定:任一任务出错都会中断整个组合。
监听任务:watch 的先后顺序
gulp.task('watch', gulp.series( 'default', function() { gulp.watch('./src/libs/*.js', gulp.series('load-lib-files', 'write-versions')); gulp.watch('./src/versions/*.js', gulp.series('load-lib-files', 'write-versions')); } ));这里有个刻意设计的细节:watch任务先执行default一次,把 libs 与 versions 都载入内存,再启动监听。因为后续监听回调复用了memory缓存,若内存中没有初始数据,write-versions会拿到空对象。gulp.watch()支持 globs 与组合任务,事件触发时默认有 200ms 延迟合并、queue排队等行为,具体选项见 docs/api/watch.md。
与源码的印证:gulp 为何能“凭空造流”
从仓库源码看,这种做法的可行性根植于 gulp 对 Vinyl 的抽象:
- 在 index.js 中,
Gulp.prototype.src = vfs.src; Gulp.prototype.dest = vfs.dest;,即src()/dest()来自vinyl-fs这个“Vinyl 适配器”(详见 docs/api/concepts.md)。src()只是“产生 Vinyl 对象”的一种来源,而非唯一来源——只要流中流动的是合法 Vinyl 对象,dest()并不关心它来自磁盘还是内存。 - 在 docs/api/vinyl.md 中,Vinyl 被定义为“虚拟文件格式”,
src()读取文件时生成 Vinyl 对象,包含路径、内容与元数据;当需要自行创建 Vinyl 对象时,应使用外部的vinyl模块。vinyl-source-stream正是这条思路的实践:它用假路径创建 Vinyl,把写入的内容填进contents。 - 在 docs/api/dest.md 中,
dest()的职责是“把 Vinyl 对象写到文件系统”,并在写盘后更新对象的cwd、base、path与stat。这说明整条管道的语义是“Vinyl 对象流”,入口无关紧要。 - 仓库 package.json 中的依赖
vinyl-fs、undertaker、glob-watcher分别支撑了src/dest、任务编排与watch,而配方用到的vinyl-source-stream、vinyl-buffer、gulp-tap、event-stream属于生态插件,与本仓库无直接耦合——这也解释了为何该配方可以独立于 gulp 核心版本演进。
其他可行方案:直接用vinyl模块构造
配方给出的是一条“插件组合”路线。若希望更底层地控制,也可以脱离vinyl-source-stream,直接用vinyl模块构造 Vinyl 对象并放入流中(Readable或through2.obj):
const Vinyl = require('vinyl'); const { Readable } = require('stream'); const file = new Vinyl({ path: 'final.version.1.js', contents: Buffer.from('...拼接好的内容...') }); const stream = Readable.from([file]); stream.pipe(dest('output'));这种写法更贴近 docs/api/vinyl.md 的官方用法(new Vinyl({ path, contents })),适合需要精细控制cwd、base、stat等元数据的场景;而配方中的vinyl-source-stream路线胜在写法直观、与流式管道衔接自然。二者本质相同:向管道注入携带内容与路径的 Vinyl 对象。
小结
本配方展示了 gulp 管道的一种通用能力:管道的输入不必来自文件系统。通过vinyl-source-stream(或vinyl模块)为内存内容配上假文件名,写入后经vinyl-buffer规整,再交给gulp.dest()落盘,即可完成“从 Buffer 构建流”。配套要点包括:
- 用
gulp-tap把流中文件内容缓存进内存对象,实现跨任务共享; - 用
process.nextTick延迟stream.end(),保证写入先于结束; - 用
event-stream的merge合并多个流作为任务的异步完成信号; - 用
series/parallel精确控制加载与写出的先后关系; - 用“先跑一遍
default再watch”保证内存缓存就绪。
掌握这一模式后,凡是“数据已在内存、却想复用 gulp 管道与插件生态”的场景——模板渲染、代码拼接、动态生成清单文件等——都可以绕开磁盘中转,直接用 gulp 的流式能力完成。
【免费下载链接】gulpA toolkit to automate & enhance your workflow项目地址: https://gitcode.com/gh_mirrors/gu/gulp
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考