【天赢金创】探究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-stream和vinyl-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)

其中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 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,path和contents也正是这个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了。到这里,你也知道了为何有contents、path这样的多个属性了。

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

相关文章
相关标签/搜索