探究Gulp的Stream

來自Gulp的難題

描述Gulp的項目構建過程的代碼,並不老是簡單易懂的。javascript

好比Gulp的這份recipecss

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-streamvinyl-buffer?它們是什麼?git

  • 添加在中間的.on('error', gutil.log)有什麼做用?github

要回答這些問題,就須要對Gulp作更深刻的瞭解,這能夠分紅幾個要素。npm

要素之一:Stream

你可能也在最初開始使用Gulp的時候就據說過:Gulp是一個有關Stream數據流)的構建系統。這句話的意思是,Gulp自己使用了Node的Streamgulp

Stream如其名字所示的「流」那樣,就像是工廠的流水線。你要加工一個產品,不用所有在一個位置完成,而是能夠拆分紅多道工序。產品從第一道工序開始,第一道工序完成後,輸出而後流入第二道工序,而後再第三道工序...一方面,大批量的產品需求也不用等到所有完工(這一般好久),而是能夠完工一個就拿到一個。另外一方面,複雜的加工過程被分割成一系列獨立的工序,這些工序能夠反覆使用,還能夠在須要的時候進行替換和重組。這就是Stream的理念。api

Stream在Node中的應用十分普遍,幾乎全部Node程序都在某種程度上用到了Stream。

管道

Stream有一個很基本的操做叫作管道pipe)。Stream是水流,而管道能夠從一個流的輸出口,接到另外一個流的輸入口,從而控制流向。若是用前面的流水線工序來講的話,就是鏈接工序的傳輸帶了。

pipes

Node的Stream有一個方法pipe(),也就是管道操做對應的方法。它通常這樣用:

src.pipe(dst)

其中srcdst都是stream,分別表明源和目標。也就是說,流src的輸出,將做爲輸入轉到流dst。此外,這個方法返回目標流(好比這裏.pipe(dst)返回dst),所以能夠鏈式調用:

a.pipe(b).pipe(c).pipe(d)

內存操做

Stream的整個操做過程,都在內存中進行。所以,相比Grunt,使用Stream的Gulp進行多步操做並不須要建立中間文件,能夠省去額外的srcdest

事件

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的rfs.createWriteStream()建立了Writable Stream的w,而後r.pipe(w)這個管道方法就能夠完成數據從rw的流動。

如前文所說,Stream是EventEmitter的實例,所以這裏的on()方法爲w添加了事件偵聽,事件finish是Writable Stream的一個事件,觸發於寫入操做完成。

更多有關Stream的介紹,推薦閱讀Stream HandbookStream API

要素之二:Vinyl文件系統

雖然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,pathcontents也正是這個Object的屬性。

Vinyl的意義

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了。到這裏,你也知道了爲何有contentspath這樣的多個屬性了。

vinyl-fs

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能夠有三種類型StreamBuffer(二進制數據)、Null(就是JavaScript裏的null)。須要注意的是,各種Gulp插件雖然操做的都是Vinyl File Object,但可能會要求不一樣的類型

在使用Gulp過程當中,可能會碰到incompatible streams的問題,像這樣:

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 buffersDealing with streams這樣兩種類型的參考。

Stream轉換

爲了讓Gulp能夠更多地利用當前Node生態體系的Stream,出現了許多Stream轉換模塊。下面介紹一些比較經常使用的。

vinyl-source-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-buffer接收Vinyl File Object做爲輸入,而後判斷其contents類型,若是是Stream就轉換爲Buffer。

不少經常使用的Gulp插件如gulp-sourcemaps、gulp-uglify,都只支持Buffer類型,所以vinyl-buffer能夠在須要的時候派上用場。

Gulp錯誤處理

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-streamvinyl-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

相關文章
相關標籤/搜索