
Gulp 插件开发完全指南从 Transform 流到可发布的高质量插件【免费下载链接】gulpA toolkit to automate enhance your workflow项目地址: https://gitcode.com/gh_mirrors/gu/gulp本篇指南以 Gulp 官方《Writing a plugin》文档为主线完整覆盖插件的工作原理、基础骨架、文件内容修改、三种 Vinyl 内容形态Buffer/Stream/null的处理并结合仓库中的编写准则、测试方法与推荐模块帮助你从零写出一个符合 Gulp 生态规范、可测试、可发布的插件。读完本文你将能够独立实现自己的gulp-*插件并理解 gulp 本身index.js是如何通过vinyl-fs与插件流协同工作的。插件到底做了什么object mode 的 Transform 流Gulp 的整个构建体系建立在流stream之上。一个 Gulp 插件本质上是一个以对象模式object mode运行的 Transform 流它做的事情只有两件接收 Vinyl File 对象虚拟文件对象描述文件的元数据与内容输出 Vinyl File 对象通过transform.push()或插件回调函数返回。这类流被称为transform streams有时也叫 through streams它既可读又可写在对象被穿过它的过程中对其进行加工。你可以在仓库的 API 概念文档 中看到 Vinyl 的定义——Vinyl 是描述一个文件核心属性是path与contents的元数据对象src()产生的就是这种对象插件处理的也正是这种对象。所有 Gulp 插件都可以归结为下面这个最小骨架var Transform require(stream).Transform; module.exports function() { // 对 Transform 打补丁或创建自己的子类 // 实现 _transform()可选实现 _flush() var transformStream new Transform({ objectMode: true }); /** * param {Buffer|string} file * param {string} encoding - 当 file 内容是 Buffer 时忽略 * param {function(Error, object)} callback - 处理完当前 chunk 后调用 * 第一个参数为错误可选第二个参数为输出数据 */ transformStream._transform function(file, encoding, callback) { var error null, output doSomethingWithTheFile(file); callback(error, output); }; return transformStream; };你也可以把 transform 与 flush 函数直接传给Transform构造函数或用 ES6 class 继承Transform。不过绝大多数插件作者更愿意使用through2模块来简化代码var through require(through2); // npm install --save through2 module.exports function() { return through.obj(function(file, encoding, callback) { callback(null, doSomethingWithTheFile(file)); }); };从through()返回的流以及 transform 函数内部的this是Transform类的实例它继承自Duplex、Readable并寄生继承Writable最终继承自 Node 的Stream。如果你需要解析额外的选项可以直接调用through()函数return through({ objectMode: true /* 其他选项... */ }, function(file, encoding, callback) { ... });through() 支持的选项选项默认值说明highWaterMark16内部缓冲区的最高水位线high water markdefaultEncodingutf8默认编码encodingutf8、base64、utf16le、ucs2等若指定会为流挂载一个StringDecoder解码器readable是否可读布尔值writable是否可写布尔值allowHalfOpen若设为false当可写侧结束时可读侧也会自动结束反之亦然修改文件内容_transform、_flush、this.push与callback传给through.obj()的函数就是_transform函数它负责加工输入的file。如果你需要在流结束时再输出一些额外数据可以提供一个可选的_flush函数。在 transform 函数内部可以通过调用this.push(file)0 次或多次来把加工/克隆后的文件传下去如果你把所有输出都交给了callback()就不需要再调用this.push(file)。关键规则只有当当前文件流或缓冲区被完全消费时才调用callback遇到错误时把错误作为callback的第一个参数传入否则传null如果所有输出数据都已经通过this.push()传走callback的第二个参数可以省略。通常一个 Gulp 插件会更新file.contents然后二选一调用callback(null, file)或调用一次this.push(file)。如果一个插件要从单个输入文件生成多个文件就需要多次调用this.push()module.exports function() { /** * this {Transform} */ var transform function(file, encoding, callback) { var files splitFile(file); this.push(files[0]); this.push(files[1]); callback(); }; return through.obj(transform); };仓库中的 gulp-unzip 就是一个多次调用push()的典型例子它还在 Vinyl transform 函数内部使用了带_flush()的 chunk transform 流。由于不能输出外部链接这里仅作原理性描述一次解压一个压缩文件、输出多个解压结果文件的插件就属于这种一进多出模式。三种file.contents形态与统一处理Vinyl 文件的contents属性可能有三种形态Stream流——见 处理流Buffer缓冲区——见 使用 Buffer空null——用于 rimraf、clean 这类不需要内容只删除或清理的场景。下面是一个简单的示例演示如何检测并处理每种形态更详细的说明见上面链接的文档var PluginError require(plugin-error); // consts var PLUGIN_NAME gulp-example; module.exports function() { return through.obj(function(file, encoding, callback) { if (file.isNull()) { // 无需处理 return callback(null, file); } if (file.isStream()) { // file.contents 是 Stream this.emit(error, new PluginError(PLUGIN_NAME, Streams not supported!)); // 或者如果你能处理 Stream //file.contents file.contents.pipe(... //return callback(null, file); } else if (file.isBuffer()) { // file.contents 是 Buffer this.emit(error, new PluginError(PLUGIN_NAME, Buffers not supported!)); // 或者如果你能处理 Buffer //file.contents ... //return callback(null, file); } }); };关于return callback(...)的说明浏览其他 Gulp 插件代码以及上面的示例时你可能会注意到 transform 函数经常直接返回回调的调用结果return callback(null, file);不要被迷惑——Gulp 会忽略 transform 函数的任何返回值。上面的代码只是下面这种写法的简写形式if (someCondition) { callback(null, file); return; } // 继续执行...两种写法等价只是前者更简洁。插件真正传递数据的通道是callback与this.push()而不是返回值。插件编写准则让插件符合 Gulp 之道官方为插件作者提供了 15 条准则详见 指南属于必读内容。虽然这些准则是可选的但官方强烈建议遵守因为它们保证了插件生态的一致性与质量插件不应做那些用现成 Node 模块就能轻易完成的事——例如删除文件夹不需要做成插件在任务里使用del这样的模块即可。为了包装而包装会污染生态。Gulp 插件是给基于文件的操作用的如果发现自己在硬把一个复杂流程塞进流里那就做成普通 Node 模块。gulp-coffee是好的插件例子coffee-script 模块不能直接处理 Vinyl于是把它包装起来补齐了这层适配。一个插件只做一件事并把它做好——避免用配置选项让插件承担完全不同任务比如 JS 压缩插件不该加添加文件头的选项。不做其他插件该做的事——不拼接那是gulp-concat的职责、不加头gulp-header、不加尾gulp-footer。如果某个常见但可选的用法需要配合其他插件就把它作为文档说明在插件内部复用其他插件能减少代码量并保证生态稳定。插件必须有测试——测试 Gulp 插件很简单甚至不需要 gulp 本身。在package.json中加入gulpplugin关键词这样插件会出现在官方插件搜索中。插件 API 应该是返回流的函数——需要存储状态时在内部存需要在插件之间传递状态/选项时挂在 file 对象上。不要在流内部 throw 错误——应通过error事件发出在流外部如创建流时配置非法可以 throw。错误信息加上插件名前缀——例如gulp-replace: Cannot do regexp replace on a stream用 plugin-error 模块可以轻松做到。插件命名以gulp-开头如果不是 Gulp 插件则不应以gulp-开头。file.contents的类型进出一致——进来是 null未读取就忽略并传下去进来是 Stream 而你又不支持就发出错误。不要把 Stream 缓冲成 Buffer 来强行适配这会导致严重后果。处理完 file 对象之前不要把它传给下游。克隆或基于文件创建新文件时使用file.clone()。优先使用推荐模块列表中的模块。不要把gulp作为依赖dependencies或对等依赖peerDependencies——用它测试或自动化你的插件流程完全可以但要放在 devDependencies 里。把 gulp 作为插件依赖意味着每个安装插件的用户都会连带安装一个完整的 gulp 及其依赖树。插件代码里没有任何理由需要 require gulp。一个好插件的完整范例以下是官方指南给出的完整示例gulp-prefixer给文件内容加前缀// through2 是 node transform stream 的轻量封装 var through require(through2); var PluginError require(plugin-error); // Consts const PLUGIN_NAME gulp-prefixer; function prefixStream(prefixText) { var stream through(); stream.write(prefixText); return stream; } // 插件级函数处理文件 function gulpPrefixer(prefixText) { if (!prefixText) { throw new PluginError(PLUGIN_NAME, Missing prefix text!); } prefixText new Buffer(prefixText); // 预先分配 // 创建每个文件都会穿过的流 return through.obj(function(file, enc, cb) { if (file.isNull()) { // 原样返回空文件 return cb(null, file); } if (file.isBuffer()) { file.contents Buffer.concat([prefixText, file.contents]); } if (file.isStream()) { file.contents file.contents.pipe(prefixStream(prefixText)); } cb(null, file); }); } // 导出插件主函数 module.exports gulpPrefixer;注意这里三个准则的落地缺少参数时在创建流之前throw允许处理文件时的错误走 error 事件gulp-prefixer命名以gulp-开头。实战一基于 Buffer 的插件如果你的插件依赖的是基于 Buffer 的库通常你会围绕file.contents作为 Buffer 来设计。下面实现一个给文件前插文本的插件详见 使用 Buffervar through require(through2); var PluginError require(plugin-error); // consts const PLUGIN_NAME gulp-prefixer; // 插件级函数处理文件 function gulpPrefixer(prefixText) { if (!prefixText) { throw new PluginError(PLUGIN_NAME, Missing prefix text!); } prefixText new Buffer(prefixText); // 预先分配 // 创建每个文件都会穿过的流 var stream through.obj(function(file, enc, cb) { if (file.isStream()) { this.emit(error, new PluginError(PLUGIN_NAME, Streams are not supported!)); return cb(); } if (file.isBuffer()) { file.contents Buffer.concat([prefixText, file.contents]); } // 确保文件继续流向下一个 gulp 插件 this.push(file); // 告诉流引擎本文件已处理完毕 cb(); }); // 返回文件流 return stream; } // 导出插件主函数 module.exports gulpPrefixer;在 gulpfile 中使用var gulp require(gulp); var gulpPrefixer require(gulp-prefixer); gulp.src(files/**/*.js) .pipe(gulpPrefixer(prepended string)) .pipe(gulp.dest(modified-files));注意上面这个插件在使用gulp.src的非缓冲流式模式时会报错。src()的buffer选项默认是true把文件内容缓冲进内存若设为falseVinyl 对象的contents会是一个暂停的流——见 src() 文档 中的选项表。如果可能插件最好也支持流模式见下一节。实战二支持流模式的插件官方高度推荐插件支持流。下面这个gulp-prefixer版本支持file.contents的所有形态详见 处理流var through require(through2); var PluginError require(plugin-error); // consts const PLUGIN_NAME gulp-prefixer; function prefixStream(prefixText) { var stream through(); stream.write(prefixText); return stream; } // 插件级函数处理文件 function gulpPrefixer(prefixText) { if (!prefixText) { throw new PluginError(PLUGIN_NAME, Missing prefix text!); } prefixText new Buffer(prefixText); // 预先分配 // 创建每个文件都会穿过的流 var stream through.obj(function(file, enc, cb) { if (file.isBuffer()) { this.emit(error, new PluginError(PLUGIN_NAME, Buffers not supported!)); return cb(); } if (file.isStream()) { // 定义转换内容的流 var streamer prefixStream(prefixText); // 捕获 streamer 的错误转成 gulp 插件错误发出 streamer.on(error, this.emit.bind(this, error)); // 开始转换 file.contents file.contents.pipe(streamer); } // 确保文件继续流向下一个 gulp 插件 this.push(file); // 告诉流引擎本文件已处理完毕 cb(); }); // 返回文件流 return stream; } // 导出插件主函数 module.exports gulpPrefixer;在流式模式下使用注意{ buffer: false }var gulp require(gulp); var gulpPrefixer require(gulp-prefixer); gulp.src(files/**/*.js, { buffer: false }) .pipe(gulpPrefixer(prepended string)) .pipe(gulp.dest(modified-files));流处理的两个关键点用file.contents.pipe(streamer)把内容流接到转换流上必须给streamer挂上error监听并把错误重新发射为 Gulp 插件错误this.emit.bind(this, error)否则转换流内部抛出的错误会丢失。测试插件不依赖 gulp 也能测测试是保证插件质量、建立用户信任的唯一途径。多数插件使用mocha、should和event-stream辅助测试详见 测试。由于 Gulp 插件本质上是可写可读的流测试时只需构造一个假的 Vinyl File 对象 →write()给插件流 → 监听data事件接收输出 → 断言内容。流模式测试var assert require(assert); var es require(event-stream); var File require(vinyl); var prefixer require(../); describe(gulp-prefixer, function() { describe(in streaming mode, function() { it(should prepend text, function(done) { // 创建假文件 var fakeFile new File({ contents: es.readArray([stream, with, those, contents]) }); // 创建 prefixer 插件流 var myPrefixer prefixer(prependthis); // 把假文件写入 myPrefixer.write(fakeFile); // 等待文件输出 myPrefixer.once(data, function(file) { // 确保进来是什么类型出去还是什么类型 assert(file.isStream()); // 缓冲内容以验证前缀已加上 file.contents.pipe(es.wait(function(err, data) { // 校验内容 assert.equal(data, prependthisstreamwiththosecontents); done(); })); }); }); }); });Buffer 模式测试var assert require(assert); var es require(event-stream); var File require(vinyl); var prefixer require(../); describe(gulp-prefixer, function() { describe(in buffer mode, function() { it(should prepend text, function(done) { // 创建假文件 var fakeFile new File({ contents: new Buffer(abufferwiththiscontent) }); // 创建 prefixer 插件流 var myPrefixer prefixer(prependthis); // 把假文件写入 myPrefixer.write(fakeFile); // 等待文件输出 myPrefixer.once(data, function(file) { // 确保进来是什么类型出去还是什么类型 assert(file.isBuffer()); // 校验内容 assert.equal(file.contents.toString(utf8), prependthisabufferwiththiscontent); done(); }); }); }); });两个测试都验证了准则第 10 条进来的内容类型与出去的内容类型一致。测试不需要 gulp 参与——你直接与插件流交互即可。仓库自身的测试test/index.test.js也遵循同样的思路直接 require gulp 实例断言src、dest、watch等 API 的存在性并通过子进程跑真实的 gulpfile.cjs/.mjs验证 CLI 链路。推荐模块清单遵守这份推荐模块清单可以保证不违反插件准则并保持各插件间的一致性详见 推荐模块用途推荐模块替换文件扩展名replace-ext错误处理plugin-error字符串着色chalk日期格式化dateformat显示为HH:MM:ss其中plugin-error在前面的所有示例中都在使用——它负责生成带插件名前缀、格式统一的错误对象。从仓库源码看插件在整个 gulp 中的位置为了更透彻地理解插件流可以看看本仓库gulp 5.0.1见 package.json的核心实现index.js 中Gulp类继承自undertaker任务注册系统而src、dest、symlink直接来自vinyl-fsGulp.prototype.src vfs.src;。这意味着src()返回的就是一个产出 Vinyl 对象的流插件的输入正是它产出的 Vinyl 对象整个管线可以抽象为gulp.src(globs)→ 产生 Vinyl 对象流 → 你的插件流逐个加工 →gulp.dest(folder)消费并写出到磁盘。Vinyl 对象正是这条链上所有环节共享的契约——这也是为什么插件的输入输出都必须是 Vinyl File 对象见 API 概念从 src() 文档 可知buffer选项默认true决定contents是 Buffer 还是暂停的流read: false则完全不读内容contents为 null。这三种形态与本文第三节的三种处理分支一一对应这也是插件必须对三者做分支处理的根本原因vinyl-fs是 gulp 的Vinyl 适配器adapter它暴露src(globs, [options])与dest(folder, [options])两种返回流的方法。你的插件事实上就是在补充这段src 之后、dest 之前的加工环节。因此写好一个插件的关键可以浓缩为三句话输入输出都是 Vinyl File 对象file.contents有三种形态要分别处理错误通过 error 事件而非 throw 传播。按照准则、测试与推荐模块落实这三点你的插件就具备了进入 gulp 生态的质量基础。进一步阅读插件编写准则必读使用 Buffer处理流插件测试推荐模块API 概念Vinyl、适配器、glob basesrc() API 参考buffer/read 等选项GitHub 加速计划 / gulp 仓库 的源码与 测试【免费下载链接】gulpA toolkit to automate enhance your workflow项目地址: https://gitcode.com/gh_mirrors/gu/gulp创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考