描述Gulp的項目構建過程的代碼,並不老是簡單易懂的。javascript
好比Gulp的這份recipe:css
var browserify = require('browserify'); var gulp = require('gulp'); var source = require('vinyl-source-stream'); var buffer = require('vinyl-buffer'); var uglify = require('gulp-uglify'); var sourcemaps = require('gulp-sourcemaps'); var gutil = require('gulp-util'); gulp.task('javascript', function () { var b = browserify({ entries: './entry.js', debug: true }); return b.bundle() .pipe(source('app.js')) .pipe(buffer()) .pipe(sourcemaps.init({loadMaps: true})) .pipe(uglify()) .on('error', gutil.log) .pipe(sourcemaps.write('./')) .pipe(gulp.dest('./dist/js/')); });
這是一個使用Browserify及Uglify並生成Source Map的例子。請想一下這樣幾個問題:html
b.bundle()
生成了什麼,爲何也能夠.pipe()
?java
爲何不是從gulp.src()
開始?node
爲何還要vinyl-source-stream
和vinyl-buffer
?它們是什麼?git
添加在中間的.on('error', gutil.log)
有什麼做用?github
要回答這些問題,就須要對Gulp作更深刻的瞭解,這能夠分紅幾個要素。npm
你可能也在最初開始使用Gulp的時候就據說過:Gulp是一個有關Stream(數據流)的構建系統。這句話的意思是,Gulp自己使用了Node的Stream。gulp
Stream如其名字所示的「流」那樣,就像是工廠的流水線。你要加工一個產品,不用所有在一個位置完成,而是能夠拆分紅多道工序。產品從第一道工序開始,第一道工序完成後,輸出而後流入第二道工序,而後再第三道工序...一方面,大批量的產品需求也不用等到所有完工(這一般好久),而是能夠完工一個就拿到一個。另外一方面,複雜的加工過程被分割成一系列獨立的工序,這些工序能夠反覆使用,還能夠在須要的時候進行替換和重組。這就是Stream的理念。api
Stream在Node中的應用十分普遍,幾乎全部Node程序都在某種程度上用到了Stream。
Stream有一個很基本的操做叫作管道(pipe)。Stream是水流,而管道能夠從一個流的輸出口,接到另外一個流的輸入口,從而控制流向。若是用前面的流水線工序來講的話,就是鏈接工序的傳輸帶了。
Node的Stream有一個方法pipe()
,也就是管道操做對應的方法。它通常這樣用:
src.pipe(dst)
其中src
和dst
都是stream,分別表明源和目標。也就是說,流src
的輸出,將做爲輸入轉到流dst
。此外,這個方法返回目標流(好比這裏.pipe(dst)
返回dst
),所以能夠鏈式調用:
a.pipe(b).pipe(c).pipe(d)
Stream的整個操做過程,都在內存中進行。所以,相比Grunt,使用Stream的Gulp進行多步操做並不須要建立中間文件,能夠省去額外的src
和dest
。
Node的Stream都是Node事件對象EventEmitter的實例,它們能夠經過.on()
添加事件偵聽。
你能夠查看EventEmitter的API文檔。
在如今的Node裏,Stream被分爲4類,分別是Readable(只讀)、Writable(只寫)、Duplex(雙向)、 Transform(轉換)。其中Duplex就是指可讀可寫,而Transform也是Duplex,只不過輸出是由輸入計算獲得的,所以算做Duplex的特例。
Readable Stream和Writable Stream分別有不一樣的API及事件(例如readable.read()
和writable.write()
),Duplex Stream和Transform Stream由於是可讀可寫,所以擁有前二者的所有特性。
雖然Node中能夠經過require("stream")
引用Stream,但比較少會須要這樣直接使用。大部分狀況下,咱們用的是Stream Consumers,也就是具備Stream特性的各類子類。
Node中許多核心包都用到了Stream,它們也是Stream Consumers。如下是一個使用Stream完成文件複製的例子:
var fs = require("fs"); var r = fs.createReadStream("nyanpass.txt"); var w = fs.createWriteStream("nyanpass.copy.txt"); r.pipe(w).on("finish", function(){ console.log("Write complete."); });
其中,fs.createReadStream()
建立了Readable Stream的r
,fs.createWriteStream()
建立了Writable Stream的w
,而後r.pipe(w)
這個管道方法就能夠完成數據從r
到w
的流動。
如前文所說,Stream是EventEmitter的實例,所以這裏的on()
方法爲w
添加了事件偵聽,事件finish
是Writable Stream的一個事件,觸發於寫入操做完成。
更多有關Stream的介紹,推薦閱讀Stream Handbook和Stream API。
雖然Gulp使用的是Stream,但卻不是普通的Node Stream,實際上,Gulp(以及Gulp插件)用的應該叫作Vinyl File Object Stream。
這裏的Vinyl,是一種虛擬文件格式。Vinyl主要用兩個屬性來描述文件,它們分別是路徑(path)及內容(contents)。具體來講,Vinyl並不神祕,它仍然是JavaScript Object。Vinyl官方給了這樣的示例:
var File = require('vinyl'); var coffeeFile = new File({ cwd: "/", base: "/test/", path: "/test/file.coffee", contents: new Buffer("test = 123") });
從這段代碼能夠看出,Vinyl是Object,path
和contents
也正是這個Object的屬性。
Gulp爲何不使用普通的Node Stream呢?請看這段代碼:
gulp.task("css", function(){ gulp.src("./stylesheets/src/**/*.css") .pipe(gulp.dest("./stylesheets/dest")); });
雖然這段代碼沒有用到任何Gulp插件,但包含了咱們最爲熟悉的gulp.src()
和gulp.dest()
。這段代碼是有效果的,就是將一個目錄下的所有.css
文件,都複製到了另外一個目錄。這其中還有一個很重要的特性,那就是全部原目錄下的文件樹,包含子目錄、文件名等,都原封不動地保留了下來。
普通的Node Stream只傳輸String或Buffer類型,也就是隻關注「內容」。但Gulp不僅用到了文件的內容,並且還用到了這個文件的相關信息(好比路徑)。所以,Gulp的Stream是Object風格的,也就是Vinyl File Object了。到這裏,你也知道了爲何有contents
、path
這樣的多個屬性了。
Gulp並無直接使用vinyl,而是用了一個叫作vinyl-fs
的模塊(和vinyl
同樣,都是npm)。vinyl-fs至關於vinyl的文件系統適配器,它提供三個方法:.src()
、.dest()
和.watch()
,其中.src()
將生成Vinyl File Object,而.dest()
將使用Vinyl File Object,進行寫入操做。
在Gulp源碼index.js
中,能夠看到這樣的對應關係:
var vfs = require('vinyl-fs'); // ... Gulp.prototype.src = vfs.src; Gulp.prototype.dest = vfs.dest; // ...
也就是說,gulp.src()
和gulp.dest()
直接來源於vinyl-fs。
Vinyl File Object的contents能夠有三種類型:Stream、Buffer(二進制數據)、Null(就是JavaScript裏的null
)。須要注意的是,各種Gulp插件雖然操做的都是Vinyl File Object,但可能會要求不一樣的類型。
在使用Gulp過程當中,可能會碰到incompatible streams的問題,像這樣:
這個問題的緣由通常都是Stream與Buffer的類型差別。Stream如前文介紹,特性是能夠把數據分紅小塊,一段一段地傳輸,而Buffer則是整個文件做爲一個總體傳輸。能夠想到,不一樣的Gulp插件作的事情不一樣,所以可能不支持某一種類型。例如,gulp-uglify
這種須要對JavaScript代碼作語法分析的,就必須保證代碼的完整性,所以,gulp-uglify
只支持Buffer類型的Vinyl File Object。
gulp.src()
方法默認會返回Buffer類型,若是想要Stream類型,能夠這樣指明:
gulp.src("*.js", {buffer: false})
在Gulp的插件編寫指南中,也能夠找到Using buffers及Dealing with streams這樣兩種類型的參考。
爲了讓Gulp能夠更多地利用當前Node生態體系的Stream,出現了許多Stream轉換模塊。下面介紹一些比較經常使用的。
vinyl-source-stream能夠把普通的Node Stream轉換爲Vinyl File Object Stream。這樣,至關於就能夠把普通Node Stream鏈接到Gulp體系內。具體用法是:
var fs = require("fs"); var source = require('vinyl-source-stream'); var gulp = require('gulp'); var nodeStream = fs.createReadStream("komari.txt"); nodeStream .pipe(source("hotaru.txt")) .pipe(gulp.dest("./"));
這段代碼中的Stream管道,做爲起始的並非gulp.src()
,而是普通的Node Stream。但通過vinyl-source-stream的轉換後,就能夠用gulp.dest()
進行輸出。其中source([filename])
就是調用轉換,咱們知道Vinyl至少要有contents和path,而這裏的原Node Stream只提供了contents,所以還要指定一個filename
做爲path。
vinyl-source-stream中的stream,指的是生成的Vinyl File Object,其contents類型是Stream。相似的,還有vinyl-source-buffer,它的做用相同,只是生成的contents類型是Buffer。
vinyl-buffer接收Vinyl File Object做爲輸入,而後判斷其contents類型,若是是Stream就轉換爲Buffer。
不少經常使用的Gulp插件如gulp-sourcemaps、gulp-uglify,都只支持Buffer類型,所以vinyl-buffer能夠在須要的時候派上用場。
Gulp有一個比較使人頭疼的問題是,若是管道中有任意一個插件運行失敗,整個Gulp進程就會掛掉。尤爲在使用gulp.watch()
作即時更新的時候,僅僅是臨時更改了代碼產生了語法錯誤,就可能使得watch掛掉,又須要到控制檯裏開啓一遍。
對錯誤進行處理就能夠改善這個問題。前面提到過,Stream能夠經過.on()
添加事件偵聽。對應的,在可能產生錯誤的插件的位置後面,加入on("error")
,就能夠作錯誤處理:
gulp.task("css", function() { return gulp.src(["./stylesheets/src/**/*.scss"]) .pipe(sass()) .on("error", function(error) { console.log(error.toString()); this.emit("end"); }) .pipe(gulp.dest("./stylesheets/dest")); });
若是你不想這樣本身定義錯誤處理函數,能夠考慮gulp-util的.log()
方法。
另外,這種方法可能會須要在多個位置加入on("error")
,此時推薦gulp-plumber,這個插件能夠很方便地處理整個管道內的錯誤。
聽說Gulp下一版本,Gulp 4,將大幅改進Gulp的錯誤處理功能,敬請期待。
如今,來回答本文開頭的問題吧。
b.bundle()
生成了什麼,爲何也能夠.pipe()
?b.bundle()
生成了Node Stream中的Readable Stream,而Readable Stream有管道方法pipe()
。
爲何不是從gulp.src()
開始?Browserify來自Node體系而不是Gulp體系,要結合Gulp和Browserify,適當的作法是先從Browserify生成的普通Node Stream開始,而後再轉換爲VInyl File Object Stream鏈接到Gulp體系中。
爲何還要vinyl-source-stream
和vinyl-buffer
?它們是什麼?由於Gulp插件的輸入必須是Buffer或Stream類型的Vinyl File Object。它們分別是具備不一樣功能的Stream轉換模塊。
添加在中間的.on('error', gutil.log)
有什麼做用?錯誤處理,以便調試問題。
再次確認,Gulp是一個有關Stream的構建系統。Gulp對其插件有很是嚴格的要求(看看插件指南就能夠知道),認爲插件必須專一於單一事務。這也許算是Gulp對Stream理念的推崇。
嘗試用Gulp完成更高級、更個性化的構建工做吧!
(從新編輯自個人博客,原文地址:http://acgtofe.com/posts/2015/09/dive-into-gulp-stream)