一些关于Gulp和NodeJS Stream的理解

    近期学习Gulp和Browserify,按照网上的教程可以实现两者的整合,但是总存在各种疑惑,例如,两者为什么可以整合、为什么需要一些另外的模块的辅助才能整合、以及对于NodeJS的Stream API本身的疑惑。只有弄清楚其中的原理,才能自己灵活地运用Gulp来整合一些有用的工具,也能享受Stream API的灵活性。

内容:

  • Stream API 简介
  • Gulp如何使用Stream API
  • 一些Gulp相关或Stream相关的工具

Stream API 简介

NodeJS提供了Stream API来实现基于流的IO。要注意的是,NodeJS提供的Stream API只是一套API,我们实际使用的是实现了这套API的模块,例如Socket,或者根据自己的需要实现自己的处理流的模模块。Stream最灵活的使用方式,应该就是pipe了,pipe可以将多个流拼接起来,组成一个流水线(pipeline),数据从pipeline的写入端流入,在组成pipeline的stream中以此对数据进行处理,最后从pipeline的读取端流出。

流的主要类型:

NodeJS的流主要有:Readable Stream, Writable Stream, Duplex Stream, Transform Stream 几种子类型。顾名思义,这四种流的特性如下:

  • Readable Stream: 提供从流中读取数据的API
  • Writable Stream: 提供往流中写入数据的API
  • Duplex Stream: 同时是Readable Stream又是Writable Stream
  • Transform Stream: 是一个提供了transform方法的Duplex Stream,数据通过其Writable Stream的API写入流中,数据经过transform方法处理,处理的结果可以通过Readable Stream的API读取
  • 另外可能还会看到Pass Through这种流,其实只是代表一种不作为的Transform Stream,直接将输入的数据原样输出

流的模式(即流中数据的类型)

NodeJS的流有两种模式(Mode),其实就是根据流中传递(读取,写入)的数据的类型来区分,分别是:

  • Object Mode:对象模式,即流中传递的是任意类型的JavaScript对象(null除外,因为null在流中有特殊的作用,下面会讲到)
  • Buffering Mode:Buffer模式,即流中传递的是Buffer,我们看API文档看到的chunk参数,在该模式下就是一个NodeJS的Buffer类型的对象,我们可以理解为这种模式下传递的是裸(raw)的二进制数据

一般情况下流需要在创建的时候指定其模式,一旦创建,则不再修改其模式,但是可以通过拼接转换流来进行模式转换,并得到一个新的流,下面会讲到。

null对象在流中的作用

NodeJS的Stream API规定:

  • 往Writable Stream中写入null,代表数据源的数据已经传递完了,不会再有数据写入,若在之后继续写入数据将产生错误
  • 从Readable Stream中读取到null,代表所有数据已经读取完毕,流中不会再有可读取的数据

流的拼接(pipe)

NodeJS的Readable Stream API提供了pipe方法用于流的拼接,pipe方法接受一个输出流(Writable Stream)对象作为参数,同时返回该输出流的引用,如果,该作为参数的输出流是一个Duplex Stream,也即返回值同时也是一个Readable Stream,则可以用这种方式拼接:streamA.pipe(streamB).pipe(streamC)....
拼接的起点,即第一个流,可以是只读流(即不可写的Readable Stream);拼接的终点,即最后一个流,可以是纯输出流(即不可读的Writable Stream);位于中间的流必须是Duplex Stream。

流水线(pipeline)

很多工具中有pipeline的概念,其实就是将多个流,通过pipe进行拼接,得到一个有序的流序列,数据从一端写入,依次进入每一个流(通常是transform stream)中进行处理,并从最后一个流输出。
流可以是Object Mode和Buffering Mode两种模式中的一种,不同模式的流可以通过一些transform stream进行模式的转换。

本文并不是要讲怎么去实现一个Stream,只是阐述一些概念,以便理解,真正要实现一个Stream还要详细阅读Stream API的定义和规范,了解上述的概念之后会对自己实现一个Stream有所帮助。

Gulp如何使用Stream API

上一节讲了NodeJS的Stream API的基本概念,现在我们讲以下Gulp是如何利用Stream API的。
Gulp是一个基于流的构建工具,因此十分灵活,其API也十分简单,就4个方法 src, dest, task, watch,分别用于输入数据,输出数据,定义任务,监控文件的变化并执行指定的任务。
Gulp本身只负责初始输入和最终输出,并提供了一个框架来管理任务,其实就连输入输出都可以不用Gulp来完成,这时候它就纯粹相当于一个任务管理的角色。
在输入输出之间的各种具体任务都是通过第三方或者用户自定义的流处理工具来完成的。
我们会想,Gulp什么都不做,为什么我们还要用它,要用插件或者自己写的话,还不如用功能丰富的webpack?其实Gulp比webpack灵活的地方在于用户定义的Gulp任务本身就是跑在NodeJS上的JavaScript程序,跟普通的程序没什么两样,因此及其灵活;而Webpack内置的功能很丰富,用户通过配置文件来指定其构建行为,但是太多既定的规则和内置的功能,虽然可通过loader和plugin进行扩展,但是这些东西都是webpack特有的,也就是说这些扩展都被打上了‘webpack专用’的记号。
其实,Gulp的灵活性除了体现在使用Gulp其实就是在写普通JavaScript程序这个事实之外,还体现在其可以直接使用现有的工具来完成任务,例如Browserify等,而不需要特地为这些工具开发”Gulp专用“的版本。Gulp的这个特性得益于其设计的虚拟文件格式与流的结合。
Gulp使用了Vinyl这种虚拟文件格式(github上的gulpjs/vinyl模块),来用于其输入输出。Vinyl抽象了大多数文件系统中文件的属性字段,例如文件名、路径和修改日期等等;同时,Vinyl还将文件的内容抽象成了Buffer或者Stream。抽象的好处就是让底层实际文件格式之间的差异,对上层透明,也就是说Gulp只认识Vinyl这种文件格式,我们只要通过一些Adapter将其他形式的文件(甚至不需要是真的文件,可能只是一个数据流或者一个Buffer)转换成Vinyl格式,便可以被Gulp处理。理论上,对于任意现有的第三方工具,我们只要Vinyl格式转换成其可以处理的格式,并将其输出转换成Vinyl格式,便可以在Gulp中使用,而由于格式转换的工作可以交给独立的转换模块来完成,所以我们可以不加修改就在Gulp中使用丰富的第三方工具来完成我们的任务。
Gulp实际处理的是流,准确地讲,是Object Mode的流,而且Object的类型是Vinyl。
在github中,我们可以看到一些Gulp专用工具的项目已经被废弃,例如gulp-browserify,而推荐直接使用非Gulp专用版本的工具,例如node-browserify。开发和维护Gulp专用版本的工具费时费力,而且经常会出现落后于独立工具版本的情况,例如工具A已经到了3.0版本,但是Gulp专用的gulp-A中使用的A工具才2.5,跟不上主流。
下一节会介绍一些转换Vinyl格式的工具。

一些Gulp相关或Stream相关的工具

  • vinyl-fs: 用于读取指定路径的文件并封装成vinyl格式的对象流,或者将vinyl对象流写入文件系统指定路径,该工具该支持glob;该工具其实是gulp的src和dest的底层实现
  • vinyl-buffer: 读取一个vinyl对象流,并将流中的vinyl对象的内容(即contents属性,该工具主要是针对contents为Stream的对象,对于contents为Buffer类型的,则原样输出)全部读取并封装到一个Buffer中,返回一个相同的vinyl对象,但是将其contents换成封装好的Buffer
  • vinyl-source-stream: 用于将一个Stream封装成一个vinyl对象,即创建一个vinyl对象,给它指定一个文件名,并将其contents设置为该Stream;需要注意的是,由于要封装的流本身只是一个流,并不是一个文件,所以这里指定的文件名是由用户随意指定的,可以说是假的文件名,但是这个假的文件名也可以被下游的输出流利用,例如使用该vinyl对象的文件名和路径将文件写到实际的文件系统中
  • bl(buffer-list): 用于从一个输入流中读取所有buffer直到不再有新的数据,并按顺序拼接成单一个Buffer,并通过回调交给调用者;目前的vinyl-buffer的实现就是用了这个工具
  • through/through2: 用于方便的创建一个transform stream,只需要指定要创建的流的模式和transform方法,就可以得到一个可用的转换流,而不需要自己去实现繁琐的流的读写控制,以及由于NodeJS历史原因造成的各种兼容性问题,许多流相关的工具都是基于该模块完成的
  • concat-stream: 用于拼接多个stream,与pumpify类似,可用于创建pipeline
  • pumpify: 同上
  • ordered-read-streams: 用于读取多个指定顺序的Readable Stream的内容,并按顺序传递到给用户,由于多个流本身的读取是异步的,所以不容易做到这一点,我们可以选择将所有的stream的内容读取完,然后在排个序交给下游,但是这个工具很巧妙地让数据可以尽快地流入下游而不需要等待所有stream都读取完,有兴趣可以欣赏以下它的源码

以上都是个人学习总结出来的内容,理解和表述的专业性可能不强,作为个人笔记,也希望能帮助到有需要的人。

posted @ 2017-05-10 17:19  waychan23  阅读(1239)  评论(0编辑  收藏  举报