前言
gulp 是前端构建工具之一,通过管道传输,插件构建,强调的是规范化流程, 这一点是它与webpack的本质上的区别之处。由于管道传输的是文件流,决定了gulp插件是针对流的操作,所以其本身模块标准是CommonJS(当然也可以使用 ESM,但是需要插件转译,例如: @esbuild-kit/cjs-loader,在运行gulpfile文件之前引用该模块,例如:gulp --require @esbuild-kit/cjs-loader
)。按任务执行(执行顺序有:series(顺序)和 parallel(并行),可理解为物理电路上的串联和并联)。
具体任务流程大致分三步:(1)首先使用 src(globs[,options]) (通配符glob)创建一个流(将文件转为二进制流);(2)其次通过 pipe(function) 对入流操作(插件) 再流出(pipe 接收一个处理流的方法,控制让流进去,再流出,即将两个流连接起来,再流出一个流);(3)最后使用 dest(directory[,options]) 将流写入到文件系统,任务结束。流动过程如下:
glob 通配符
-
字符串片段与分割符:字符串片段指两个分隔符之间的所有字符组成的字符串,在 glob 中分割符永远是 / 字符,不区分操作系统,将 // 字符作转义符(在Windows中,Node 使用 // 作为路径分割符,避免直接使用 path.join、dirname 和 filename、process.cwd() ),否则是无效的 glob,例如:
glob_with_uncommon_\*_character.js
; -
一个星号(在一个字符串片段中匹配任意数量的字符,包括零个匹配):匹配当前目录下的一级文件,不能匹配嵌套的文件,例如,
src/*.scss
,只能匹配src/button.scss,不能匹配到src/dark/css-vars.scss,src/dark/test/index.scss; -
两个星号(在多个字符串片段中匹配任意数量的字符,包括零个匹配):匹配嵌套目录,例如,
src/**/*.scss
,可以匹配src/button.scss、src/dark/css-vars.scss,src/dark/test/index.scss; -
!取反:由于 glob 匹配时是按照每个 glob 在数组中的位置依次进行匹配操作的,所以 glob 的数组中的取反(negative) glob 必须跟在一个非取反(non-negative)的 glob 后面。第一个 glob 匹配到一组匹配项,然后后面的取反 glob 删除这些匹配项中的一部分。如果取反 glob 只是由普通字符组成的字符串,则执行效率是最高的。例如:
['script/**/*.js', '!script/vendor/']、['**/*.js', '!node_modules/']
;
Node.js 流
Node.js 的流是二进制数据(Node.js 内置模块 Buffer 可创建流,存储二进制数据),有四种类型:
- 可读流 Readable:例如 fs.createReadStream(path) ;
- 可写流 Writable:例如 fs.createWriteStream(path) ;
- 可读写流 Duplex;
- 转换流 Transform:在读写过程中可修改和变换数据的 Duplex流 ,对流操作再流出。
创建流的方式
有三种创建流的方式:
-
Node.js 文件系统 fs 提供了两个API可创建一个可读流(fs.createReadStream(path[, options]) )和可写流(fs.createWriteStream(path[, options]) );
-
Node 内置模块 Stream(无需额外安装,可直接使用):new Stream.Readable、new Stream.Writable、new Stream.Duplex 和 new Stream.Transform,重写相应的方法,可参考Node.js Stream(流);
// 创建可读流 const readableStream = new Stream.Readable() console.log('可读流:', readableStream) readableStream.__read = ()=> {} readableStream.push("哈喽,露水晰123!") readableStream.push(null) // 创建可读流 const writable = new Stream.Writable({ write(chunk, encoding, callback) { console.log(chunk.toString()) callback() // 表示写入已完成 } }) writable.write("哈喽,露水晰123!") writable.end() // 创建可读写流 const duplex = new Stream.Duplex({ read() {}, write(chunk, encoding, callback) { this.push(chunk.toString().split("").reverse().join("")) callback() // 表示写入已完成 } }) duplex.push("Hello LuShuiXi!") duplex.push(null) duplex.on("data", chunk=> console.log(chunk.toString())) // 监听流上的事件 // 创建转换流 const { createReadStream, createWriteStream } = require("fs") // Node.js 内置模块 fs const transform = new Stream.Transform({ transform(chunk, encoding, callback) { this.push(chunk.toString().split("").reverse().join("")) // 将数据反转 callback() // 表示转换操作已完成 } }) const input = createReadStream("test.txt") // 创建一个可读流 const output = createWriteStream("button.css") // 创建一个可写流 input.pipe(transform).pipe(output)
-
第三方插件,例如 through2(A tiny wrapper around Node.js streams.Transform (Streams2/3) to avoid explicit subclassing noise)专门创建转换流。
const through = require('through2') const transform = through.obj(function(file, encoding, callback){...})
gulp 插件
gulp 插件的实质是Node.js 转换流(Transform Stream) ,理解两点:其一,gulp 插件 是一个流(返回值是一个流);其二,该流是转换流(四大流之一,对流进行操作再流出)。gulp 插件作用根据需求操作经过流(由 pipe 传输控制),可以更改该经过流的每个文件的文件名(如:修改文件名(gulp-reaname,file.path = Path.join(file.base, path)
))、元数据或文件内容(如:压缩样式(gulp-clean-css,file.contents = new Buffer.from(css.styles)
,更改了文件内容))等。
想学习如何开发Gulp插件,在前面了解了Gulp和插件的实质、作用及流的相关点后,开始看下成熟的插件的代码,从代码入手。首先找到 Gulp 插件列表,可以看到当前共有4264个插件。先从常用的插件入手,例如 gulp-sass(转译scss/sass为css,更改文件内容和文件后缀名称)、gulp-autoprefixer(给CSS添加浏览器兼容)、gulp-clean-css(压缩css)、gulp-rename(修改文件名称,例如给文件名称加上前缀) ,将其代码下载到本地调试学习。
非常简单的插件示例 PluginA
自定义插件pluginA,其功能:修改入流的文件名为${Date.now()}.css
// gulp 插件,修改文件名称
const Stream = require('stream')
const Path = require('path')
function PluginA() {
// 创建一个转换流
const stream = new Stream.Transform({ objectMode: true })
// 如果入流是一个列表,则会逐个进入转换流, 由管道负责流的接入
// originalFile 入流
// callback 出流, 表示转换操作完成后的回调
stream._transform = function(originalFile, unused, callback) {
const file = originalFile.clone({ contents: false })
// 对流的具体操作:修改文件名称
file.path = Path.join(file.base, `${Date.now()}.css`)
// 操作完成后,立即调用callback,表示操作完成了
callback(null, file) // 把处理后的流流出
// callback() // 没有给管道输出任何流,后面收到空流
}
return stream
}
module.exports = PluginA
使用该插件,编写 gulpfile 文件,使用 ESM 模块标准,修改src/*.css文件名称为${Date.now()}.css
,且输出到dist目录。
import path from "path"
import { parallel, src, pipe, dest } from "gulp"
import pluginA from "./plugin-a"
export function updateStyleName() {
const result = src(path.resolve(__dirname, "src/*.css")) // 创建一个可读流(ReadableStream)
.pipe(pluginA()) // 自定义插件,修改文件名称
.pipe(dest(path.resolve(__dirname, 'dist')))
// console.log('result:', result)
return result
}
const build = parallel(
updateStyleName,
)
export default build
执行结果如下