在上一篇文章我們對 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/)上,可自行下載除錯。共勉~