从零单排之gulp实战
更新日期:
作为一个DOTA小菜鸟,一直羡慕各路大神的从零单排,在DOTA上是单排不了了。所以就只能代码上单排了。这个系列准备写一些各种知识的从零开始到深入原理的过程。记录学到的知识。
这篇文章就介绍下gulp实战。
node stream
gulp是基于node的stream之上的,所以在介绍gulp之前。需要先介绍下nodejs里面流的概念。
以前第一次接触流还是写java的时候,那时候文件都是流。现在node里面也有了流的概念。
这里可以把读文件比作搬水桶,没有流的话我们读文件是一次性把水桶里面的水都搬走,而有了流就相当于我们搞了个管道。让水一点点的流出来。然后后面要处理的时候,我们也可以一点点的去处理。比如一个文件非常大的时候,一次性的读到内存是很消耗性能的。这时候我们就可以使用流一点点的取,一点点的处理,最后再一点点的写到文件里面。
node里面最典型的http请求就是一个流。读文件也可以以流的形式。所有的流都继承了EventEmitter,所以都可以使用简单的事件机制。
比如一个简单的文件读取流:
1 2 3 4 5 6 7 8 9 | var stream = fs.createReadStream('ex.txt'); //阶段性的触发data事件,像流水一样,每次出来一点点数据 stream.on('data',function(chunk){ console.log(chunk); }) //全部读取完毕后触发end事件 stream.on('end',function(chunk){ console.log('done'); }) |
node中的流
node里面有四种类型的流:Readable,Writable,Duplex,Transform
1.Readable 可读流
上面的createReadStream就是生成了一个可读流。
一个可读流分为两种模式:流动模式和暂停模式。流动模式就是数据会自动往外读取。暂停模式是默认的模式,代表数据不会自动往外读。
当处于暂停模式时我们必须使用stream.read(size)来主动读取数据。一直到没有数据的时候会返回null;
也可以开启流动模式,有三种方法:
(1) 监听data事件,就像我们上面的使用那样。没有数据会触发end事件
(2) 使用pipe,会自动开启。pipe代表把当前流写入到一个可写流(见下面介绍)里面。比如我们可以这样:
1 2 3 4 | var readStream = fs.createReadStream('a.txt'); var writeStream = fs.createWriteStream('b.txt'); readStream.pipe(writeStream); //这样 a.txt的内容就会自动写入到 b.txt里面。 |
(3)使用stream.resume()来开启流动模式,有时我们不需要监听data事件来获取获取数据,只是想要监听end事件,这时候就可以使用这个来开启流动模式。
2.Writable 可写流,
比如createWriteStream就是返回一个可写流。用于接受一个可读流的写入。就好像是在两个缸直接加了条管道。
可写流也支持一些方法。
首先就像我们上面做的那样我们可以 writeStream.pipe(writeStream);直接把一个可读流接到一个可写流上面。
当然有的时候我们可以自己手动去写数据。
writeStream.write(data, encoding, callback);用于向可写流里面写数据。如果数据量太大,处理不过来会返回false。这个时候先不要写数据了。我们可以监听 drain事件,代表处理完毕可以继续写数据了。
另外调用writeStream.end(chunk, encoding, callback);来表示写数据完毕。
3.Transform 转换流,同时可读可写,他可以接受一个流然后做一系列的操作后再输出处理后的流。相当于一个中转站。你可以想象为,它的左边是一个可写流接受可读流的写入。进行处理后,它的右边是个可读流,给下一个转换流处理或者给下一个可写流。所以我们可以这么用readStream.pipe(transformStream).pipe(writeStream);
4.Duplex 双工流,也是同时可读可写。可以作为可读流也可以作为可写流,典型的例子是socket,zlib的使用。这边的gulp涉及不到就不详细展开了。简单来说就是可以这么用 duplexStream.pipe(transformStream).pipe(duplexStream);
四种流构造函数
上面的可读流可写流我们都是直接用的系统自带的,其实我们是可以自己创建一个自定义流对象的。node提供了下面各自对应的父类函数。我们需要自定义自己的流类来继承这些基类并且实现对应的方法。
模式 | 父类名 | 需要继承的方法 | 解释 |
---|---|---|---|
Reading only | require(‘stream’).Readable | _read | 每次可读流开始读数据的时候就会调用_read来获取数据,在_read里面可以调用this.push(data)来将数据输出到流。如果data是null就代表没数据了 |
Writing only | require(‘stream’).Writable | _write | 每次可写流写数据的时候就会调用_write来写数据,我们可以决定这些数据去哪 |
Reading and writing | require(‘stream’).Duplex | _read, _write | 双工流可以同时继承这两个函数,表现出上面的样子 |
Operate on written data, then read the result | require(‘stream’).Transform | _transform, _flush | Transform(chunk, encoding, callback)用于处理上一个可读流过来的数据,chunk就是数据,可以在Transform中多次使用this.push(data)来将数据写到下一个流,调用callback结束当前转换。_flush(callback)是在数据全部读完后会调用,在_flush里面也可以调用push和callback结束转换。 |
这边与下面gulp最相关的就是转换流,我们以一个例子来说明主要的用法。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | var Transform = require('stream').Transform; util.inherits(TestStream, Transform); function TestStream(options) { Transform.call(this, options); } TestStream.prototype._transform = function(chunk, encoding, cb) { this.push(chunk+'|'); cb(); }; TestStream.prototype._flush = function(cb) { cb('end'); }; //于是可以使用 readStream.pipe(new TestStream()).pipe(writeStream); |
其他模式的都差不多一个用法,总之就是继承父类再实现相对应的接口。
默认情况下上面四种构造函数实例化后都是使用的string或者buffer来传递数据的。其实 在使用new实例化对象的时候支持objectMode参数。如果这个参数为true。那么我们调用this.push的时候就可以传入一个对象。这样后面的流去分段读的时候每次都会只读取一个对象。有了这个特性,我们就可以在不同的流之间以对象为单位进行数据转换了。
gulp简单介绍
前端经过这几年的发展已经越来越向工业化的道路发展了。java有ant,ruby有rake。而前端比较出名的任务工具就要属grunt和gulp了。说白了这两个工具就是可以自定义任务脚本,然后使用命令行去执行这些任务。包括打包,压缩,开个服务器等等等都可以实现。
入门使用
首先我们安装gulp:
1 | npm install -g gulp
|
记得使用全局安装,这样我们就具有了gulp这个命令。
下面我们需要在项目根目录新建一个gulpfile.js,这个是任务的定义的地方。
我们看一个简单的gulpfile.js:
1 2 3 4 5 6 7 8 | var gulp = require('gulp'); var less = require('gulp-less'); gulp.task('less',[], function () { gulp.src('./public/css/*.less') .pipe(less({})) .pipe(gulp.dest('./public/css/')); }); |
这边引用了gulp-less,所以不要忘了 npm install gulp-less —save
less就是官方的一个插件,用于将less格式的文件解析为css。可以看到这边less({})其实就是返回了一个转换流。
gulp.task用于定义一个新的任务。
第一个参数是这个任务的名称是less.
第二个可选参数是当前任务依赖的其他任务,启动当前任务会同时挨个启动依赖的任务。
第三个可选参数是任务的具体执行函数。
gulp.src用于读取文件,转换为流的形式。写入下一个转换流。参数为glob标识,或者多个标识组成的数组
什么是glob呢?
说白了就是支持那种类似正则一样的标识手法,比如.less就代表所有以less结尾的文件。
另外我们经常在unix类型的系统里面使用 ls .less 也是使用的golb标识。
gulp主要使用的是node-glob来解析glob的,所以可以去那边看一下支持的写法。
gulp.dest执行返回一个转换流,用于接受可读流然后写到对应的文件里面。接受一个文件夹地址作为参数。
于是我们定义了一个起始输入流,还有最后面的转换流。使用了less在半路上处理中间的流。想要其他的插件也只需要中间多pipe一个就行了。只有src还有dest会有磁盘读写,而且职责清晰,每个人对经过的流都做些操作再丢给下一个处理。这就像车间流水线一样,每个阶段都有相应的处理程序对原材料加工。所以效率相当高效。
最后我们在命令行里调用:
1 | gulp less
|
这样就会把./public/css/
下面的所有less文件解析为对应文件名的css。
gulp还支持两个接口
一个是watch用于监控文件变化:接受glob标识,还有相对应的执行任务数组
1 2 3 4 | gulp.task('watch_less', function () { gulp.watch(['./public/css/*.less'], ['less']); }); //运行gulp watch_less 会监听所有的less文件,发生改变就会调用less任务。 |
还有一个是run用于手动调用任务,不过即将被废弃了,慎用。
1 2 3 4 | gulp.task('test',[less],function(){ gulp.run('watch_less'); }) //在运行完less后 运行watch_less持续监听文件 |
gulp顺序执行
刚开始接触gulp的人肯定会被他的异步机制搞晕。在grunt里面因为后面一个脚本的读入文件都是依赖前一个脚本的产出文件的,所以他们是串行的,因此任务会一个个的去执行。
但是在gulp里所有任务都是并行同步执行的。比如我们上面的例子:
1 2 3 4 | gulp.task('test',[less],function(){ gulp.run('watch_less'); }) //在运行完less后 运行watch_less持续监听文件 |
我们是希望 运行完less,再去运行watch_less,但是在gulp里面这两个任务是同时并行执行的。这会给我们造成困扰,这个时候我们需要对依赖的任务做一些处理。使得依赖的任务做完了再去执行当前任务的执行函数。
gulp有三种方式:
1.直接返回一个流
1 2 3 4 | gulp.task('watch_less', function () { return gulp.watch(['./public/css/*.less'], ['less']); }); //是的只要加一个return就好了 |
2.直接返回一个promise
比如我们再定义一个异步的任务
1 2 3 4 5 6 7 8 9 10 | gulp.task('test_async', function () { var Q = require('q'); var deferred = Q.defer(); // do async stuff setTimeout(function () { deferred.resolve(); }, 1); return deferred.promise; }); |
3.使用回调callback
task的执行函数其实都有个回调,我们只需要在异步队列完成的时候调用它就好了。
1 2 3 4 5 6 7 | gulp.task('test_async', function (cb) { // do async stuff setTimeout(function () { cb() }, 1); }); |
所以只要依赖的任务是上面三种情况之一,就能保证当前任务在依赖任务执行完成后再执行。这边需要注意的是依赖的任务相互之间还是并行的。需要他们按顺序的话。记得给每个依赖的任务也配置好依赖关系。
好了基本上gulp的入门使用就介绍完了。
gulp vs grunt
grunt由于是基于文件读写的,前一个任务做完写好文件,下个任务再去读文件进行操作。所以频繁的磁盘io读写会很浪费性能。而且由于grunt的输入输出格式没有统一规范化,导致目前线上的各种库的入口文件还有出口文件配置五花八门,而且职责混乱。越来越被人诟病。
于是gulp出来了。基于node的stream机制,最大程度的减少了磁盘io消耗。所有的插件具有了统一的入口,插件都是在流的中间对输入流操作,输出更改过的流给下一个插件。并且每个人只做自己的事,职责分明。
当然grunt出来的比较早,所以官方社区的插件比较多。gulp的插件还没那么多。不过未来gulp肯定是主流,目前我们团队的项目基本都在望gulp上迁移了。
gulp原理及插件编写
原理一览
gulp的原理其实很简单,如果去查看它的源码,你就会发现他就像用乐高积木搭建起来的一样。都是已经很成熟的一个个模块组合在一起的。
主要有两大模块支撑着gulp.
第一个就是一个任务依赖队列的库。orchestrator.主要实现了task方法,还有解决任务依赖关系。这个没啥好说的
第二个就是一个文件转换系统。vinyl-fs.主要实现了src,dst还有watch。watch功能就不说了使用的glob-watcher。src内置了glob的功能,前面已经解释过了。
src会将匹配到的路径全部生成一个对应的开启Object Mode
的可读流。传递的对象为vinyl,如下参数:
1 2 3 4 5 6 7 | {
cwd: cwd,
base: base,
stat: 使用 fs.Stats得到的结果
path: 文件路径,
contents: 文件的内容
}
|
之后再把所有的可读流合并成一个可读流,使用的是ordered-read-streams
然后传递给一个转换流,也就是我们之后会编写的插件。处理完再写入到一个dst生成的转换流里面,写入文件。整个过程就结束了。
由于dst是一个转换流,所以可以继续pipe到其他的插件上。
插件编写
终于到了最后一步了,我们可以自己编写插件了。通过前面的解释其实已经很清楚了,我们只要实现一个转换流就好了。为了避免写那么一大堆的继承的东西我们可以使用一些库,比如through2,比如through-gulp
于是我们可以写个最简单的插件,比如我写的这个简单的将html文件内容打包成cmd,amd的模块。方便线上跨域调用的插件。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 | /** * 用于将html代码打包成cmd,amd规范可以使用的模块。这样可以跨域使用。 */ var through = require('through-gulp'); function viewComile() { //through(transformFunction, flushFunction) through 接受两个参数对应前面的_transform, _flush。返回一个生成好的开启object mode的转换流 var stream = through(function(file, encoding,callback) { var htmlStr,jsStr; //拿到内容 htmlStr = file.contents.toString(); /******逻辑开始*****/ //各种逻辑,处理htmlStr,略 /******逻辑结束*****/ //改写文件内容 file.contents = new Buffer(htmlStr); //给下一个流 this.push(file); callback(); }); return stream; }; module.exports = viewComile; |
一个插件就弄好了,至于后续是发到npm上还是放在本地目录,就随便了。
可以直接放在本地,调用的时候就可以:
1 2 3 4 5 6 7 8 9 10 | var rename = require('gulp-rename'); var viewCompile = require('./task/gulp-view-compile.js'); gulp.task('viewcompile', function () { gulp.src('./public/views/**/*.html') .pipe(viewCompile()) .pipe(rename(function (path) { path.extname = ".js"; })) .pipe(gulp.dest('build/views/')) }); |
当然也可以封装成npm包。比如上面的插件我最后封装在了这里,可以到这里面看看详细用法。
结语
写文章好累,一个知识点总是串联着其他的知识点,想要重头开始理一边的确很吃力,不过相应的带来的收获也是巨大的