要说到 gulp 的运作方式,就不得不提到 vinyl 和 Node.js 的 stream
vinyl
vinyl 是 gulp 用的虛擬的檔案格式,在它的 readme 是這麼說的:「說你檔案你第一個想到的是什麼?路徑跟內容吧」,它主要紀錄的資訊有:
vinyl 是 gulp 所使用的虚拟的文件格式,在它的自述文件是这么说的:“当提到文件时你首先想到的是什么?肯定是路径和内容吧”,它主要记录的信息有:
- path:文件路径
- contents:文件内容
- cwd:程序执行的目录
- base:用 glob 寻找文件时开始的目录,例如 src/**/*.js,那 base 就会是 src,这可以用来重现目录结构
另外它还有几个函数用来判断这个文件的内容是什么类型的这类操作,到于这个虚拟文件实际上用在什么地方,咱们稍后再说,先创建一个文件试试:
const { readFile } = require('fs/promises') const Vinyl = require('vinyl') const file = new Vinyl({ path: __filename, // 这个文件的路径 // 在 Node.js 14.8.0 中 支持 top level await,不过还是建议用 async function 封装 contents: await readFile(__filename), cwd: process.cwd(), // 不设定的话默认值为 process.cwd() base: process.cwd(), // 不设定的话默认值为 process.cwd() }) console.log(file) // 这是应该能看到 <File "file.js" <Buffer ...>>
这样就创建好了一个 gulp 用的文件格式,接下来是 Node.js 的 stream。
stream
stream 设计的本意是要处理大文件的,它能一次读取文件的一小部分,然后再传给调用者进行处理:
const { createReadStream } = require('fs') // 创建一个读取文件的 stream const stream = createReadStream(__filename) // 设置文件字符集编码,否则就会以 Buffer (二进制数据) 的格式读取 stream.setEncoding('utf-8') // 处理数据 stream.on('data', (chunk) => { console.log('chunk', JSON.stringify(chunk)) }) // 结束 stream.on('end', () => { console.log('end') })
实际上它是基于 EventEmitter 之上创建的一组 API,比如 on 就是来自于 EventEmitter,只要照着它的模式,也不一定只能传小块的文件,在 Node.js 中的 stream 也有一个对象模式,如果传的数据不是缓冲区或流就应该设置为对象模式,而对象模式跟一般的模式主要的区别就是不需要处理字符集编码。
再回到 gulp,还记得之前说过 src 是回传一个 stream 吗?接下来看看里面到底传的是什么,先写个 gulpfile 来试看看:
exports.stream = function () { const stream = src('./src/**.js') stream.on('data', (data) => { console.log(data) }) return stream }
输出:
<File "file.js" <Buffer ...>>
没错,这就是 Vinyl 的文件,gulp 用 stream 的对象模式在传输这些文件,plugin 其实上就是回传一个 Transform 的 stream(Node.js 中 stream 的一种,stream 的种类有 Readable、Writeable、Duplex、Transform)来转换这些文件,比下面是一个把文件内容都换成大写的流:
const { Transform } = require('stream') exports.uppercase = function () { return src('./src/**.js') .pipe( new Transform({ // 设置为 object mode objectMode: true, transform(file, _enc, cb) { // 把文件内容转换成为字符串 const content = file.contents.toString() // 转为大写后再转回 Buffer 存回去 file.contents = Buffer.from(content.toUpperCase()) // 用 callback 回传 cb(null, file) }, }) ) .pipe(dest('upper')) }
现在我们有了一个把文件全转大写的 plugin了,不过没什么实用上的意义。这样转来转去的效率太低了。
剩下的部分就是 gulp 处理任务的注册与依赖性的逻辑了,依赖性主要是由 undertaker 处理的,不过我觉得这里没什么特别的东西,所以有兴趣就自己去看看吧。