在上一篇文章我们对 Stream 的特性及其接口进行了介绍,gulp 之所以在性能上好于 grunt,主要是因为有了 Stream 助力来做数据的传输和处理。
那么我们不难猜想出,在 gulp 的任务中,gulp.src 接口将匹配到的文件转化为可读(或 Duplex/Transform)流,通过 .pipe 流经各插件进行处理,最终推送给 gulp.dest 所生成的可写(或 Duplex/Transform)流并生成文件。
本文将追踪 gulp(v4.0)的源码,对上述猜想进行验证。
为了分析源码,我们打开 gulp 仓库下的入口文件 index.js,可以很直观地发现,几个主要的 API 都是直接引用 vinyl-fs 模块上暴露的接口的:
1 2 3 4 5 6 7 8 9 10 11 12 |
var util = require('util'); var Undertaker = require('undertaker'); var vfs = require('vinyl-fs'); var watch = require('glob-watcher'); //略... Gulp.prototype.src = vfs.src; Gulp.prototype.dest = vfs.dest; Gulp.prototype.symlink = vfs.symlink; //略... |
因此了解 vinyl-fs 模块的作用,便成为掌握 gulp 工作原理的关键之一。需要留意的是,当前 gulp4.0 所使用的 vinyl-fs 版本是 v2.0.0。
vinyl-fs 其实是在 vinyl 模块的基础上做了进一步的封装,在这里先对它们做个介绍:
一. Vinyl
Vinyl 可以看做一个文件描述器,通过它可以轻松构建单个文件的元数据(metadata object)描述对象。依旧是来个例子简洁明了:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
//ch2-demom1 var Vinyl = require('vinyl'); var jsFile = new Vinyl({ cwd: '/', base: '/test/', path: '/test/file.js', contents: new Buffer('abc') }); var emptyFile = new Vinyl(); console.dir(jsFile); console.dir(emptyFile); |
上述代码会打印两个File文件对象:
简而言之,Vinyl 可以创建一个文件描述对象,通过接口可以取得该文件所对应的数据(Buffer类型)、cwd路径、文件名等等:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
//ch2-demo2 var Vinyl = require('vinyl'); var file = new Vinyl({ cwd: '/', base: '/test/', path: '/test/newFile.txt', contents: new Buffer('abc') }); console.log(file.contents.toString()); console.log('path is: ' + file.path); console.log('basename is: ' + file.basename); console.log('filename without suffix: ' + file.stem); console.log('file extname is: ' + file.extname); |
打印结果:
更全面的 API 请参考官方描述文档,这里也对 vinyl 的源码贴上解析注释:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 |
var path = require('path'); var clone = require('clone'); var cloneStats = require('clone-stats'); var cloneBuffer = require('./lib/cloneBuffer'); var isBuffer = require('./lib/isBuffer'); var isStream = require('./lib/isStream'); var isNull = require('./lib/isNull'); var inspectStream = require('./lib/inspectStream'); var Stream = require('stream'); var replaceExt = require('replace-ext'); //构造函数 function File(file) { if (!file) file = {}; //-------------配置项缺省设置 // history是一个数组,用于记录 path 的变化 var history = file.path ? [file.path] : file.history; this.history = history || []; this.cwd = file.cwd || process.cwd(); this.base = file.base || this.cwd; // 文件stat,它其实就是 require('fs').Stats 对象 this.stat = file.stat || null; // 文件内容(这里其实只允许格式为 stream 或 buffer 的传入) this.contents = file.contents || null; this._isVinyl = true; } //判断是否 this.contents 是否 Buffer 类型 File.prototype.isBuffer = function() { //直接用 require('buffer').Buffer.isBuffer(this.contents) 做判断 return isBuffer(this.contents); }; //判断是否 this.contents 是否 Stream 类型 File.prototype.isStream = function() { //使用 this.contents instanceof Stream 做判断 return isStream(this.contents); }; //判断是否 this.contents 是否 null 类型(例如当file为文件夹路径时) File.prototype.isNull = function() { return isNull(this.contents); }; //通过文件 stat 判断是否为文件夹 File.prototype.isDirectory = function() { return this.isNull() && this.stat && this.stat.isDirectory(); }; //克隆对象,opt.deep 决定是否深拷贝 File.prototype.clone = function(opt) { if (typeof opt === 'boolean') { opt = { deep: opt, contents: true }; } else if (!opt) { opt = { deep: true, contents: true }; } else { opt.deep = opt.deep === true; opt.contents = opt.contents !== false; } // 先克隆文件的 contents var contents; if (this.isStream()) { //文件内容为Stream //Stream.PassThrough 接口是 Transform 流的一个简单实现,将输入的字节简单地传递给输出 contents = this.contents.pipe(new Stream.PassThrough()); this.contents = this.contents.pipe(new Stream.PassThrough()); } else if (this.isBuffer()) { //文件内容为Buffer /** cloneBuffer 里是通过 * var buf = this.contents; * var out = new Buffer(buf.length); * buf.copy(out); * 的形式来克隆 Buffer **/ contents = opt.contents ? cloneBuffer(this.contents) : this.contents; } //克隆文件实例对象 var file = new File({ cwd: this.cwd, base: this.base, stat: (this.stat ? cloneStats(this.stat) : null), history: this.history.slice(), contents: contents }); // 克隆自定义属性 Object.keys(this).forEach(function(key) { // ignore built-in fields if (key === '_contents' || key === 'stat' || key === 'history' || key === 'path' || key === 'base' || key === 'cwd') { return; } file[key] = opt.deep ? clone(this[key], true) : this[key]; }, this); return file; }; /** * pipe原型接口定义 * 用于将 file.contents 写入流(即参数stream)中; * opt.end 用于决定是否关闭 stream */ File.prototype.pipe = function(stream, opt) { if (!opt) opt = {}; if (typeof opt.end === 'undefined') opt.end = true; if (this.isStream()) { return this.contents.pipe(stream, opt); } if (this.isBuffer()) { if (opt.end) { stream.end(this.contents); } else { stream.write(this.contents); } return stream; } // file.contents 为 Null 的情况不往stream注入内容 if (opt.end) stream.end(); return stream; }; /** * inspect原型接口定义 * 用于打印出一条与文件内容相关的字符串(常用于调试打印) * 该方法可忽略 */ File.prototype.inspect = function() { var inspect = []; // use relative path if possible var filePath = (this.base && this.path) ? this.relative : this.path; if (filePath) { inspect.push('"'+filePath+'"'); } if (this.isBuffer()) { inspect.push(this.contents.inspect()); } if (this.isStream()) { //inspectStream模块里有个有趣的写法——判断是否纯Stream对象,先判断是否Stream实例, //再判断 this.contents.constructor.name 是否等于'Stream' inspect.push(inspectStream(this.contents)); } return '<File '+inspect.join(' ')+'>'; }; /** * 静态方法,用于判断文件是否Vinyl对象 */ File.isVinyl = function(file) { return file && file._isVinyl === true; }; // 定义原型属性 .contents 的 get/set 方法 Object.defineProperty(File.prototype, 'contents', { get: function() { return this._contents; }, set: function(val) { //只允许写入类型为 Buffer/Stream/Null 的数据,不然报错 if (!isBuffer(val) && !isStream(val) && !isNull(val)) { throw new Error('File.contents can only be a Buffer, a Stream, or null.'); } this._contents = val; } }); // 定义原型属性 .relative 的 get/set 方法(该方法几乎不使用,可忽略) Object.defineProperty(File.prototype, 'relative', { get: function() { if (!this.base) throw new Error('No base specified! Can not get relative.'); if (!this.path) throw new Error('No path specified! Can not get relative.'); //返回 this.path 和 this.base 的相对路径 return path.relative(this.base, this.path); }, set: function() { //不允许手动设置 throw new Error('File.relative is generated from the base and path attributes. Do not modify it.'); } }); // 定义原型属性 .dirname 的 get/set 方法,用于获取/设置指定path文件的文件夹路径。 // 要求初始化时必须指定 path <或history> Object.defineProperty(File.prototype, 'dirname', { get: function() { if (!this.path) throw new Error('No path specified! Can not get dirname.'); return path.dirname(this.path); }, set: function(dirname) { if (!this.path) throw new Error('No path specified! Can not set dirname.'); this.path = path.join(dirname, path.basename(this.path)); } }); // 定义原型属性 .basename 的 get/set 方法,用于获取/设置指定path路径的最后一部分。 // 要求初始化时必须指定 path <或history> Object.defineProperty(File.prototype, 'basename', { get: function() { if (!this.path) throw new Error('No path specified! Can not get basename.'); return path.basename(this.path); }, set: function(basename) { if (!this.path) throw new Error('No path specified! Can not set basename.'); this.path = path.join(path.dirname(this.path), basename); } }); // 定义原型属性 .extname 的 get/set 方法,用于获取/设置指定path的文件扩展名。 // 要求初始化时必须指定 path <或history> Object.defineProperty(File.prototype, 'extname', { get: function() { if (!this.path) throw new Error('No path specified! Can not get extname.'); return path.extname(this.path); }, set: function(extname) { if (!this.path) throw new Error('No path specified! Can not set extname.'); this.path = replaceExt(this.path, extname); } }); // 定义原型属性 .path 的 get/set 方法,用于获取/设置指定path。 Object.defineProperty(File.prototype, 'path', { get: function() { //直接从history出栈 return this.history[this.history.length - 1]; }, set: function(path) { if (typeof path !== 'string') throw new Error('path should be string'); // 压入history栈中 if (path && path !== this.path) { this.history.push(path); } } }); module.exports = File; |
二. Vinyl-fs
Vinyl 虽然可以很方便地来描述一个文件、设置或获取文件的内容,但还没能便捷地与文件系统进行接入。
我的意思是,我们希望可以使用通配符的形式来简单地匹配到咱想要的文件,把它们转为可以处理的 Streams,做一番加工后,再把这些 Streams 转换为处理完的文件。
Vinyl-fs 就是实现这种需求的一个 Vinyl 适配器,我们看看它的用法:
1 2 3 4 5 6 7 8 9 10 11 |
var map = require('map-stream'); var fs = require('vinyl-fs'); var log = function(file, cb) { console.log(file.path); cb(null, file); }; fs.src(['./js/**/*.js', '!./js/vendor/*.js']) .pipe(map(log)) .pipe(fs.dest('./output')); |
如上方代码所示,Vinyl-fs 的 .src 接口可以匹配一个通配符,将匹配到的文件转为 Vinyl Stream,而 .dest 接口又能消费这个 Stream,并生成对应文件。
这里需要先补充一个概念 —— .src 接口所传入的“通配符”有个专有术语,叫做 GLOB,我们先来聊聊 GLOB。
GLOB 可以理解为我们给 gulp.src 等接口传入的第一个 pattern 参数的形式,例如“./js/**/*.js”,另外百度百科的“glob模式”描述是这样的:
所谓的 GLOB 模式是指 shell 所使用的简化了的正则表达式:
⑴ 星号(*)匹配零个或多个任意字符;
⑵ [abc]匹配任何一个列在方括号中的字符(这个例子要么匹配一个 a,要么匹配一个 b,要么匹配一个 c);
⑶ 问号(?)只匹配一个任意字符;
⑷ 如果在方括号中使用短划线分隔两个字符,表示所有在这两个字符范围内的都可以匹配(比如 [0-9] 表示匹配所有 0 到 9 的数字)。
在 vinyl-fs 中,是使用 glob-stream <v5.0.0>通过算法(minimatch)来解析 GLOB 的,它会拿符合上述 GLOB 模式规范的 pattern 参数去匹配相应的文件,:
1 2 3 4 5 6 7 |
var gs = require('glob-stream'); var stream = gs.create('./files/**/*.coffee', {options}); stream.on('data', function(file){ // file has path, base, and cwd attrs }); |
而 glob-stream 又是借助了 node-glob 来匹配文件列表的:
1 2 3 4 5 6 7 8 9 10 11 |
//ch2-demo3 var Glob = require("glob").Glob; var path = require('path'); var pattern = path.join(__dirname, '/*.txt'); var globber = new Glob(pattern, function(err, matches){ console.log(matches) }); globber.on('match', function(filename) { console.log('matches file: ' + filename) }); |
打印结果:
这里也贴下 glob-stream 的源码注解:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 |
'use strict'; var through2 = require('through2'); var Combine = require('ordered-read-streams'); var unique = require('unique-stream'); var glob = require('glob'); var micromatch = require('micromatch'); var resolveGlob = require('to-absolute-glob'); var globParent = require('glob-parent'); var path = require('path'); var extend = require('extend'); var gs = { // 为单个 glob 创建流 createStream: function(ourGlob, negatives, opt) { // 使用 path.resolve 将 golb 转为绝对路径(加上 cwd 前缀) ourGlob = resolveGlob(ourGlob, opt); var ourOpt = extend({}, opt); delete ourOpt.root; // 通过 glob pattern 生成一个 Glob 对象(属于一个事件发射器<EventEmitter>) var globber = new glob.Glob(ourGlob, ourOpt); // 抽取出 glob 的根路径 var basePath = opt.base || globParent(ourGlob) + path.sep; // Create stream and map events from globber to it var stream = through2.obj(opt, negatives.length ? filterNegatives : undefined); var found = false; //Glob 对象开始注册事件 globber.on('error', stream.emit.bind(stream, 'error')); globber.once('end', function() { if (opt.allowEmpty !== true && !found && globIsSingular(globber)) { stream.emit('error', new Error('File not found with singular glob: ' + ourGlob)); } stream.end(); }); //注册匹配到文件时的事件回调 globber.on('match', function(filename) { //标记已匹配到文件(filename 为文件路径) found = true; //写入流(触发 stream 的 _transform 内置方法) stream.write({ cwd: opt.cwd, base: basePath, path: path.normalize(filename) }); }); return stream; //定义 _transform 方法,过滤掉排除模式所排除的文件 function filterNegatives(filename, enc, cb) { //filename 是匹配到的文件对象 var matcha = isMatch.bind(null, filename); if (negatives.every(matcha)) { cb(null, filename); //把匹配到的文件推送入缓存(供下游消费) } else { cb(); // 忽略 } } }, // 为多个globs创建流 create: function(globs, opt) { //预设参数处理 if (!opt) { opt = {}; } if (typeof opt.cwd !== 'string') { opt.cwd = process.cwd(); } if (typeof opt.dot !== 'boolean') { opt.dot = false; } if (typeof opt.silent !== 'boolean') { opt.silent = true; } if (typeof opt.nonull !== 'boolean') { opt.nonull = false; } if (typeof opt.cwdbase !== 'boolean') { opt.cwdbase = false; } if (opt.cwdbase) { opt.base = opt.cwd; } //如果 glob(第一个参数)非数组,那么把它转为 [glob],方便后续调用 forEach 方法 if (!Array.isArray(globs)) { globs = [globs]; } var positives = []; var negatives = []; var ourOpt = extend({}, opt); delete ourOpt.root; //遍历传入的 glob globs.forEach(function(glob, index) { //验证 glob 是否有效 if (typeof glob !== 'string' && !(glob instanceof RegExp)) { throw new Error('Invalid glob at index ' + index); } //是否排除模式(如“!b*.js”) var globArray = isNegative(glob) ? negatives : positives; // 排除模式的 glob 初步处理 if (globArray === negatives && typeof glob === 'string') { // 使用 path.resolve 将 golb 转为绝对路径(加上 cwd 前缀) var ourGlob = resolveGlob(glob, opt); //micromatch.matcher(ourGlob, ourOpt) 返回了一个方法,可传入文件路径作为参数,来判断是否匹配该排除模式的 glob(即返回Boolean) glob = micromatch.matcher(ourGlob, ourOpt); } globArray.push({ index: index, glob: glob }); }); //globs必须最少有一个匹配模式(即非排除模式)的glob,否则报错 if (positives.length === 0) { throw new Error('Missing positive glob'); } // 只有一条匹配模式,直接生成流并返回 if (positives.length === 1) { return streamFromPositive(positives[0]); } // 创建 positives.length 个独立的流(数组) var streams = positives.map(streamFromPositive); // 这里使用了 ordered-read-streams 模块将一个数组的 Streams 合并为单个 Stream var aggregate = new Combine(streams); //对合成的 Stream 进行去重处理(以“path”属性为指标) var uniqueStream = unique('path'); var returnStream = aggregate.pipe(uniqueStream); aggregate.on('error', function(err) { returnStream.emit('error', err); }); return returnStream; //返回最终匹配完毕(去除了排除模式globs的文件)的文件流 function streamFromPositive(positive) { var negativeGlobs = negatives.filter(indexGreaterThan(positive.index)) //过滤,排除模式的glob必须排在匹配模式的glob后面 .map(toGlob); //返回该匹配模式glob后面的全部排除模式globs(数组形式) return gs.createStream(positive.glob, negativeGlobs, opt); } } }; function isMatch(file, matcher) { //matcher 即单个排除模式的 glob 方法(可传入文件路径作为参数,来判断是否匹配该排除模式的 glob) //此举是拿匹配到的文件(file)和排除模式GLOP规则做匹配,若相符(如“a/b.txt”匹配“!a/c.txt”)则为true if (typeof matcher === 'function') { return matcher(file.path); } if (matcher instanceof RegExp) { return matcher.test(file.path); } } function isNegative(pattern) { if (typeof pattern === 'string') { return pattern[0] === '!'; } if (pattern instanceof RegExp) { return true; } } function indexGreaterThan(index) { return function(obj) { return obj.index > index; }; } function toGlob(obj) { return obj.glob; } function globIsSingular(glob) { var globSet = glob.minimatch.set; if (globSet.length !== 1) { return false; } return globSet[0].every(function isString(value) { return typeof value === 'string'; }); } module.exports = gs; |
留意通过 glob-stream 创建的流中,所写入的数据:
1 2 3 4 5 |
stream.write({ cwd: opt.cwd, base: basePath, path: path.normalize(filename) }); |
是不像极了 Vinyl 创建文件对象时可传入的配置。
我们回过头来专注 vinyl-fs 的源码,其入口文件如下:
1 2 3 4 5 6 7 |
'use strict'; module.exports = { src: require('./lib/src'), dest: require('./lib/dest'), symlink: require('./lib/symlink') }; |
下面分别对这三个对外接口(也直接就是 gulp 的对应接口)进行分析。
2.1 gulp.src
该接口文件为 lib/src/index.js,代码量不多,但引用的模块不少。
主要功能是使用 glob-stream 匹配 GLOB 并创建 glob 流,通过 through2 写入 Object Mode 的 Stream 去,把数据初步加工为 Vinyl 对象,再按照预设项进行进一步加工处理,最终返回输出流:
代码主体部分如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 |
function createFile(globFile, enc, cb) { //通过传入 globFile 来创建一个 vinyl 文件对象 //并赋予 cb 回调(这个回调一看就是 transform 的格式,将vinyl 文件对象注入流中) cb(null, new File(globFile)); } function src(glob, opt) { // 配置项初始化 var options = assign({ read: true, buffer: true, sourcemaps: false, passthrough: false, followSymlinks: true }, opt); var inputPass; // 判断是否有效的 glob pattern if (!isValidGlob(glob)) { throw new Error('Invalid glob argument: ' + glob); } // 通过 glob-stream 创建匹配到的 globStream var globStream = gs.create(glob, options); //加工处理生成输出流 var outputStream = globStream //globFile.path 为 symlink的情况下,转为硬链接 .pipe(resolveSymlinks(options)) //创建 vinyl 文件对象供下游处理 .pipe(through.obj(createFile)); // since 可赋与一个 Date 或 number,来要求指定某时间点后修改过的文件 if (options.since != null) { outputStream = outputStream // 通过 through2-filter 检测 file.stat.mtime 来过滤 .pipe(filterSince(options.since)); } // read 选项默认为 true,表示允许文件内容可读(为 false 时不可读 且将无法通过 .dest 方法写入硬盘) if (options.read !== false) { outputStream = outputStream //获取文件内容,写入file.contents 属性去。 //预设为 Buffer 时通过 fs.readFile 接口获取 //否则为 Stream 类型,通过 fs.createReadStream 接口获取 .pipe(getContents(options)); } // passthrough 为 true 时则将 Transform Stream 转为 Duplex 类型(默认为false) if (options.passthrough === true) { inputPass = through.obj(); outputStream = duplexify.obj(inputPass, merge(outputStream, inputPass)); } //是否要开启 sourcemap(默认为false),若为 true 则将流推送给 gulp-sourcemaps 去初始化, //后续在 dest 接口里再调用 sourcemaps.write(opt.sourcemaps) 将 sourcemap 文件写入流 if (options.sourcemaps === true) { outputStream = outputStream .pipe(sourcemaps.init({loadMaps: true})); } globStream.on('error', outputStream.emit.bind(outputStream, 'error')); return outputStream; } module.exports = src; |
这里有个 symlink 的概念 —— symlink 即 symbolic link,也称为软链(soft link),它使用了其它文件或文件夹的链接来指向一个文件。一个 symlink 可以链接任何电脑上的任意文件或文件夹。在 Linux/Unix 系统上,symlink 可以通过 ln 指令来创建;在 windows 系统上可以通过 mklink 指令来创建。
更多 symlink 的介绍建议参考 wiki —— https://en.wikipedia.org/wiki/Symbolic_link。
2.2 gulp.dest
该接口文件为 lib/dest/index.js,其主要作用自然是根据 src 接口透传过来的输出流,生成指定路径的目标文件/文件夹:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 |
function dest(outFolder, opt) { if (!opt) { opt = {}; } // _transform 接口 function saveFile(file, enc, cb) { // 写入文件之前的准备处理,主要是 opt 初始化、file对象的 path/base/cwd 等属性 // 修改为相对 outFolder 的路径,方便后面 writeContents 生成正确的目的文件 prepareWrite(outFolder, file, opt, function(err, writePath) { if (err) { return cb(err); } //通过 fs.writeFile / fs.createWriteStream 等接口来写入和创建目标文件/文件夹 writeContents(writePath, file, cb); }); } // 生成 sourcemap 文件(注意这里的 opt.sourcemaps 若有则应为指定路径) var mapStream = sourcemaps.write(opt.sourcemaps); var saveStream = through2.obj(saveFile); // 合并为单条 duplex stream var outputStream = duplexify.obj(mapStream, saveStream); //生成目标文件/文件夹 mapStream.pipe(saveStream); //依旧返回输出流(duplex stream) return outputStream; } module.exports = dest; |
接前文的流程图:
至此我们就搞清楚了 gulp 的 src 和 dest 是怎样运作了。另外 gulp/vinyl-fs 还有一个 symlink 接口,其功能与 gulp.dest 是一样的,只不过是专门针对 symlink 的方式来处理(使用场景较少),有兴趣的同学可以自行阅读其入口文件 lib/symlink/index.js。
本文涉及的所有示例代码和源码注释文件,均存放在我的仓库(https://github.com/VaJoy/stream/)上,可自行下载调试。共勉~