探究Gulp的Stream

2020-09-30 18:01 更新

來(lái)自Gulp的難題

描述 Gulp 的項(xiàng)目構(gòu)建過(guò)程的代碼,并不總是簡(jiǎn)單易懂的。

比如 Gulp 的這份 recipe :

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/'));
});

這是一個(gè)使用 Browserify 及 Uglify 并生成 Source Map 的例子。請(qǐng)想一下這樣幾個(gè)問(wèn)題:

  • b.bundle() 生成了什么,為什么也可以 .pipe() ?
  • 為什么不是從 gulp.src() 開(kāi)始?
  • 為什么還要 vinyl-source-stream 和 vinyl-buffer ?它們是什么?
  • 添加在中間的 .on('error', gutil.log) 有什么作用?

要回答這些問(wèn)題,就需要對(duì) Gulp 做更深入的了解,這可以分成幾個(gè)要素。

要素之一:Stream

你可能也在最初開(kāi)始使用 Gulp 的時(shí)候就聽(tīng)說(shuō)過(guò):Gulp 是一個(gè)有關(guān) Stream(數(shù)據(jù)流)的構(gòu)建系統(tǒng)。這句話的意思是,Gulp 本身使用了 Node 的 Stream 。

Stream 如其名字所示的“流”那樣,就像是工廠的流水線。你要加工一個(gè)產(chǎn)品,不用全部在一個(gè)位置完成,而是可以拆分成多道工序。產(chǎn)品從第一道工序開(kāi)始,第一道工序完成后,輸出然后流入第二道工序,然后再第三道工序…一方面,大批量的產(chǎn)品需求也不用等到全部完工(這通常很久),而是可以完工一個(gè)就拿到一個(gè)。另一方面,復(fù)雜的加工過(guò)程被分割成一系列獨(dú)立的工序,這些工序可以反復(fù)使用,還可以在需要的時(shí)候進(jìn)行替換和重組。這就是 Stream 的理念。

Stream 在 Node 中的應(yīng)用十分廣泛,幾乎所有 Node 程序都在某種程度上用到了Stream 。

管道

Stream 有一個(gè)很基本的操作叫做管道(pipe)。Stream 是水流,而管道可以從一個(gè)流的輸出口,接到另一個(gè)流的輸入口,從而控制流向。如果用前面的流水線工序來(lái)說(shuō)的話,就是連接工序的傳輸帶了。

Node 的 Stream 有一個(gè)方法 pipe() ,也就是管道操作對(duì)應(yīng)的方法。它一般這樣用:

src.pipe(dst)

其中 src 和 dst 都是 stream ,分別代表源和目標(biāo)。也就是說(shuō),流 src 的輸出,將作為輸入轉(zhuǎn)到流 dst 。此外,這個(gè)方法返回目標(biāo)流(比如這里.pipe(dst)返回dst),因此可以鏈?zhǔn)秸{(diào)用:

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

內(nèi)存操作

Stream 的整個(gè)操作過(guò)程,都在內(nèi)存中進(jìn)行。因此,相比 Grunt ,使用 Stream 的 Gulp 進(jìn)行多步操作并不需要?jiǎng)?chuàng)建中間文件,可以省去額外的 src 和 dest 。

事件

Node 的 Stream 都是 Node 事件對(duì)象 EventEmitter 的實(shí)例,它們可以通過(guò) .on() 添加事件偵聽(tīng)。

你可以查看 EventEmitter的API文檔。

類(lèi)型

在現(xiàn)在的 Node 里, Stream 被分為4類(lèi),分別是 Readable(只讀)、Writable(只寫(xiě))、Duplex(雙向)、** Transform(轉(zhuǎn)換**)。其中 Duplex 就是指可讀可寫(xiě),而 Transform 也是 Duplex ,只不過(guò)輸出是由輸入計(jì)算得到的,因此算作 Duplex 的特例。

Readable Stream 和 Writable Stream 分別有不同的 API 及事件(例如 readable.read() 和 writable.write() ), Duplex Stream 和 Transform Stream 因?yàn)槭强勺x可寫(xiě),因此擁有前兩者的全部特性。

例子

雖然 Node 中可以通過(guò) require("stream") 引用 Stream ,但比較少會(huì)需要這樣直接使用。大部分情況下,我們用的是 Stream Consumers ,也就是具有 Stream 特性的各種子類(lèi)。

Node 中許多核心包都用到了 Stream ,它們也是 Stream Consumers 。以下是一個(gè)使用 Stream 完成文件復(fù)制的例子:

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() 創(chuàng)建了 Readable Stream 的r,fs.createWriteStream() 創(chuàng)建了 Writable Stream 的w,然后 r.pipe(w) 這個(gè)管道方法就可以完成數(shù)據(jù)從r到w的流動(dòng)。

如前文所說(shuō),Stream 是 EventEmitter 的實(shí)例,因此這里的 on() 方法為w添加了事件偵聽(tīng),事件 finish 是 Writable Stream 的一個(gè)事件,觸發(fā)于寫(xiě)入操作完成。

更多有關(guān) Stream 的介紹,推薦閱讀 Stream Handbook 和 Stream API 。

要素之二:Vinyl文件系統(tǒng)

雖然 Gulp 使用的是 Stream ,但卻不是普通的 Node Stream ,實(shí)際上,Gulp(以及Gulp插件)用的應(yīng)該叫做 Vinyl File Object Stream 。

這里的 Vinyl ,是一種虛擬文件格式。Vinyl 主要用兩個(gè)屬性來(lái)描述文件,它們分別是路徑(path)及內(nèi)容(contents)。具體來(lái)說(shuō),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 也正是這個(gè) Object 的屬性。

Vinyl的意義

Gulp 為什么不使用普通的 Node Stream 呢?請(qǐng)看這段代碼:

gulp.task("css", function(){
    gulp.src("./stylesheets/src/**/*.css")
        .pipe(gulp.dest("./stylesheets/dest"));
});

雖然這段代碼沒(méi)有用到任何 Gulp 插件,但包含了我們最為熟悉的 gulp.src() 和 gulp.dest() 。這段代碼是有效果的,就是將一個(gè)目錄下的全部 .css 文件,都復(fù)制到了另一個(gè)目錄。這其中還有一個(gè)很重要的特性,那就是所有原目錄下的文件樹(shù),包含子目錄、文件名等,都原封不動(dòng)地保留了下來(lái)。

普通的 Node Stream 只傳輸 String 或 Buffer 類(lèi)型,也就是只關(guān)注“內(nèi)容”。但 Gulp 不只用到了文件的內(nèi)容,而且還用到了這個(gè)文件的相關(guān)信息(比如路徑)。因此,Gulp 的 Stream 是 Object 風(fēng)格的,也就是 Vinyl File Object 了。到這里,你也知道了為什么有 contents 、 path 這樣的多個(gè)屬性了。

vinyl-fs

 Gulp 并沒(méi)有直接使用 vinyl ,而是用了一個(gè)叫做 vinyl-fs 的模塊(和vinyl一樣,都是npm)。vinyl-fs 相當(dāng)于 vinyl 的文件系統(tǒng)適配器,它提供三個(gè)方法:.src()、.dest()和 .watch(),其中 .src() 將生成 Vinyl File Object,而 .dest() 將使用 Vinyl File Object ,進(jìn)行寫(xiě)入操作。

在 Gulp 源碼 index.js 中,可以看到這樣的對(duì)應(yīng)關(guān)系:

var vfs = require('vinyl-fs');
// ...
Gulp.prototype.src = vfs.src;
Gulp.prototype.dest = vfs.dest;
// ...

也就是說(shuō),gulp.src() 和 gulp.dest() 直接來(lái)源于 vinyl-fs 。

類(lèi)型

Vinyl File Object 的 contents 可以有三種類(lèi)型:Stream、Buffer(二進(jìn)制數(shù)據(jù))、Null(就是JavaScript里的null)。需要注意的是,各類(lèi) Gulp 插件雖然操作的都是Vinyl File Object,但可能會(huì)要求不同的類(lèi)型。

在使用 Gulp 過(guò)程中,可能會(huì)碰到 incompatible streams 的問(wèn)題,像這樣:

091835

這個(gè)問(wèn)題的原因一般都是 Stream 與 Buffer 的類(lèi)型差異。Stream 如前文介紹,特性是可以把數(shù)據(jù)分成小塊,一段一段地傳輸,而B(niǎo)uffer則是整個(gè)文件作為一個(gè)整體傳輸??梢韵氲剑煌?Gulp 插件做的事情不同,因此可能不支持某一種類(lèi)型。例如, gulp-uglify 這種需要對(duì) JavaScript 代碼做語(yǔ)法分析的,就必須保證代碼的完整性,因此,gulp-uglify 只支持 Buffer 類(lèi)型的 Vinyl File Object。

gulp.src() 方法默認(rèn)會(huì)返回Buffer類(lèi)型,如果想要 Stream 類(lèi)型,可以這樣指明:

gulp.src("*.js", {buffer: false})

在 Gulp 的插件編寫(xiě)指南中,也可以找到 Using buffers 及 Dealing with streams 這樣兩種類(lèi)型的參考。

Stream轉(zhuǎn)換

為了讓 Gulp 可以更多地利用當(dāng)前 Node 生態(tài)體系的 Stream,出現(xiàn)了許多 Stream 轉(zhuǎn)換模塊。下面介紹一些比較常用的。

vinyl-source-stream

vinyl-source-stream 可以把普通的 Node Stream 轉(zhuǎn)換為 Vinyl File Object Stream。這樣,相當(dāng)于就可以把普通 Node Stream 連接到 Gulp 體系內(nèi)。具體用法是:

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。但經(jīng)過(guò) vinyl-source-stream 的轉(zhuǎn)換后,就可以用 gulp.dest() 進(jìn)行輸出。其中 source([filename]) 就是調(diào)用轉(zhuǎn)換,我們知道 Vinyl 至少要有 contents 和 path ,而這里的原 Node Stream 只提供了 contents ,因此還要指定一個(gè) filename 作為 path。

vinyl-source-stream 中的 stream,指的是生成的 Vinyl File Object ,其 contents 類(lèi)型是 Stream。類(lèi)似的,還有 vinyl-source-buffer,它的作用相同,只是生成的 contents 類(lèi)型是 Buffer。

vinyl-buffer

vinyl-buffer 接收 Vinyl File Object 作為輸入,然后判斷其 contents 類(lèi)型,如果是Stream 就轉(zhuǎn)換為 Buffer。

很多常用的 Gulp 插件如 gulp-sourcemaps 、gulp-uglify,都只支持 Buffer 類(lèi)型,因此 vinyl-buffer 可以在需要的時(shí)候派上用場(chǎng)。

Gulp錯(cuò)誤處理

Gulp 有一個(gè)比較令人頭疼的問(wèn)題是,如果管道中有任意一個(gè)插件運(yùn)行失敗,整個(gè) Gulp 進(jìn)程就會(huì)掛掉。尤其在使用 gulp.watch() 做即時(shí)更新的時(shí)候,僅僅是臨時(shí)更改了代碼產(chǎn)生了語(yǔ)法錯(cuò)誤,就可能使得 watch 掛掉,又需要到控制臺(tái)里開(kāi)啟一遍。

對(duì)錯(cuò)誤進(jìn)行處理就可以改善這個(gè)問(wèn)題。前面提到過(guò),Stream 可以通過(guò) .on() 添加事件偵聽(tīng)。對(duì)應(yīng)的,在可能產(chǎn)生錯(cuò)誤的插件的位置后面,加入 on("error"),就可以做錯(cuò)誤處理:

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"));
});

如果你不想這樣自己定義錯(cuò)誤處理函數(shù),可以考慮 gulp-util 的 .log() 方法。

另外,這種方法可能會(huì)需要在多個(gè)位置加入 on("error"),此時(shí)推薦 gulp-plumber,這個(gè)插件可以很方便地處理整個(gè)管道內(nèi)的錯(cuò)誤。

據(jù)說(shuō) Gulp 下一版本,Gulp 4,將大幅改進(jìn) Gulp 的錯(cuò)誤處理功能,敬請(qǐng)期待。

解答

現(xiàn)在,來(lái)回答本文開(kāi)頭的問(wèn)題吧。

b.bundle() 生成了什么,為什么也可以 .pipe() ?b.bundle() 生成了 Node Stream 中的 Readable Stream ,而 Readable Stream 有管道方法 pipe() 。

為什么不是從 gulp.src() 開(kāi)始?Browserify 來(lái)自 Node 體系而不是 Gulp 體系,要結(jié)合 Gulp 和 Browserify ,適當(dāng)?shù)淖龇ㄊ窍葟?Browserify 生成的普通 Node Stream 開(kāi)始,然后再轉(zhuǎn)換為 VInyl File Object Stream 連接到 Gulp 體系中。

為什么還要 vinyl-source-stream 和 vinyl-buffer ?它們是什么?因?yàn)?Gulp 插件的輸入必須是 Buffer 或 Stream 類(lèi)型的 Vinyl File Object 。它們分別是具有不同功能的 Stream 轉(zhuǎn)換模塊。

添加在中間的 .on('error', gutil.log) 有什么作用?錯(cuò)誤處理,以便調(diào)試問(wèn)題。

結(jié)語(yǔ)

再次確認(rèn),Gulp 是一個(gè)有關(guān) Stream 的構(gòu)建系統(tǒng)。Gulp 對(duì)其插件有非常嚴(yán)格的要求(看看插件指南就可以知道),認(rèn)為插件必須專(zhuān)注于單一事務(wù)。這也許算是 Gulp 對(duì)Stream 理念的推崇。

嘗試用 Gulp 完成更高級(jí)、更個(gè)性化的構(gòu)建工作吧!


以上內(nèi)容是否對(duì)您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號(hào)
微信公眾號(hào)

編程獅公眾號(hào)