gulp的流与执行顺序
gulp的流与执行顺序
gulp的关键在于流,这从它的logo就能看出来。
在node中,流是操作文件时一个重要的概念。流是指什么呢?它包含两个含义:“水流”和“流水”。 水流蕴含了源源不断或是一股一股那样流过的意味;而流水是“流水线”或是“流水作业”里那种让物件通过各个环节依次对其加工的意思。 我们经常接触到的“流媒体”主要是前者的含义,当你在线看一部电影时,影音数据从服务器源源不断地流入你的播放器, 再经过一些处理展现在你眼前;而gulp中的流我觉得含义偏重于后者,因为gulp的任务就是把源文件进行各种加工处理最终输出到指定位置。 我们说“源文件”而不是“原文件”,在gulp中,它还真是流的源头。
gulp是基于node的,但是它并没有直接使用node中fs模块里的文件系统和流,而是包装了一层vinyl。 vinyl是一个用来描述文件的简单的数据格式,通过vinyl-fs可以把node原生的文件系统封装成vinyl。 这个封装使得整个流的过程更加简单。从源头上,vinyl使用glob语法获取源,比如通过一个表达式 "src/**/*.js"就获得到了src目录下各级目录中的js文件,这要是用原生的fs恐怕得写个遍历树的算法程序了吧。 在gulp或vinyl-fs的api里,通过一个传入glob表达式的src方法就获得到了一个流的源。 很明显,在多数情况下这个源是由多个文件组成的,可以想象成这些文件构成了一个一股一股的文件流, 都将要通过一系列管道被加工处理。那么接下来就是管道,与原生的fs相同,vinyl使用管道也是用pipe方法。 pipe接受一个函数为参数,将当前流的内容传给这个函数让其加工,vinyl把流的内容封装得更加简明好用, 而且,对于调用一次pipe方法,其传入的函数会对这个流的所有文件作用,换句话说,传入pipe方法的函数实际上是针对一个文件的, 而流中所有的文件都会被这个函数加工一下。这么看,vinyl的流有些并行的感觉,但本质上说javascript是单线程的, 加工的过程还是一个接着一个进行的,所以说成让文件一个接一个地流过某个管道更确切。
既然是流,就应该有一种顺序进行的感觉。不过处理流的代码是异步的,比如下面的代码:
gulp = require('gulp')
through = require('through2')
gulp.task 'test', ->
stream = gulp.src('src/js/*.js')
.pipe through.obj (file, enc, cb) ->
console.log 'processing...'
cb null, file
.pipe(gulp.dest('test'))
console.log 'end'
如果在src/js目录下有两个js文件。执行gulp task,结果是:
[20:12:32] Starting 'test'...
end
[20:12:32] Finished 'test' after 13 ms
processing...
processing...
很显然,pipe中的函数是异步执行的。不过对于流中的一个文件,各pipe中的函数一定会按照先后顺序执行。 再来看一段代码,为了方便,我把管道中的处理函数写成一般gulp插件的形式:
processor = (info) ->
through.obj (file, enc, cb) ->
console.log file.path, info
cb null, file
gulp.task 'test', ->
stream = gulp.src('src/js/*.js')
.pipe processor("in pipe 1")
.pipe processor("in pipe 2")
.pipe processor("in pipe 3")
.pipe(gulp.dest('test'))
console.log 'end'
执行结果是:
[16:31:24] Starting 'test'...
end
[16:31:24] Finished 'test' after 9.02 ms
/src/js/city.js in pipe 1
/src/js/city.js in pipe 2
/src/js/city.js in pipe 3
/src/js/sysUtils.js in pipe 1
/src/js/sysUtils.js in pipe 2
/src/js/sysUtils.js in pipe 3
执行结果的确是像流那样一个文件挨着一个文件,一个过程接着一个过程处理完成的。尽管pipe中的函数会异步执行, 但它们严格按照先注册先执行的顺序进行。看来一个任务的执行顺序在一个流中是能够得以保证的,而且也只能在一个流中得以保证。
那么对于多个任务的情况呢?gulp.task方法可以接受一个任务数组,任务数组中的任务将会并行执行。 而传入gulp.task的函数将会在任务数组中所有任务执行完毕后开始执行。简单来说是这样的,实际要小心。
gulp的api中关于task方法有这么一项注意:“Are your tasks running before the dependencies are complete? Make sure your dependency tasks are correctly using the async run hints: take in a callback or return a promise or event stream.” 中文版本是:“你的任务是否在这些前置依赖的任务完成之前运行了?请一定要确保你所依赖的任务列表中的任务都使用了正确的异步执行方式:使用一个 callback,或者返回一个 promise 或 stream。”
既然任务中的那些处理函数一般都是异步执行的,那么怎么才能知道它们执行完了呢?只能是通过回调了, 可以是直接的回调,也可以是其它约定好的回调,也就是promise或者流的事件。 对于JavaScript异步编程来说这是很常见的事情,然而这也带来了相应的局限性。来看个例子:
gulp.task 'buildjs', (cb) ->
del.sync('prd/js')
gulp.src('src/js/**/*.js')
.pipe uglify({output:{ascii_only:true}})
.pipe gulp.dest('prd/js')
gulp.src(['src/js/**/*.*', '!src/js/**/*.js'])
.pipe gulp.dest('prd/js')
这是一个很常见的任务,就是把js代码混淆压缩,然后把非js代码原样拷贝出去。我写的是coffee版的gulpfile, coffeescript会默认把最后一个表达式作为返回值,所以这里实际上是返回了第二个流,也就是拷贝非js文件的那个流。 如果只执行这个任务倒无所谓,谁先谁后都能完成,但是如果它被作为前置任务呢?
gulp.task zip, ['buildjs'], ->
gulp.src('prd/js/*.js')
.pipe(zip('release.zip'))
.pipe(gulp.dest('prd'))
如果文件比较多的话,会发现压缩包里的文件不完整。原因就是buildjs这个任务返回的流是拷贝文件那个流, 而zip这个任务也只会等待拷贝文件完成时开始,此时混淆文件那个流还不一定能执行完。如果返回混淆文件那个流, 照常理说这个流会执行的慢一些,但仍不那么靠谱,毕竟没有逻辑保障,所以应该把它们都拆开,分别作为zip的前置任务:
gulp.task 'buildjs', ->
gulp.src('src/js/**/*.js')
.pipe uglify({output:{ascii_only:true}})
.pipe gulp.dest('prd/js')
gulp.task 'copy', ->
gulp.src(['src/js/**/*.*', '!src/js/**/*.js'])
.pipe gulp.dest('prd/js')
gulp.task 'zip', ['buildjs', 'copy'], ->
gulp.src('prd/js/*.js')
.pipe(zip('release.zip'))
.pipe(gulp.dest('prd'))
这样,zip一定会等buildjs和copy两个任务中各自的流全都执行完才会开始执行,从逻辑上也没问题了。 这样看好像是把本来可以在一个任务里完成的东西拆开了,不过gulp本身鼓励短小专一,所有的gulp插件都很小, 且只完成一件事情。这么说的话构建js文件和拷贝非js文件说是两件事也比较合理。
上例是一个两级的顺序保障,“分-总”的结构。你也许发现我偷偷地把删除目录的一句给去掉了。因为我的确不知道该把它放在哪里好。 buildjs和copy是并行的,不能确保谁先,如果放到buildjs里,万一copy先执行了,误删了已经拷贝过去的东西可不好。 所以,我需要多级的顺序保障,把删除目录的任务放在更高的一级,形成一个“总-分-总的结构”。 然而我并没找到形成这种结构的方法,貌似只能一个接着一个地进行:
gulp.task 'clean', ->
del.sync('prd/js')
gulp.task 'copy', ['clean'], ->
gulp.src(['src/js/**/*.*', '!src/js/**/*.js'])
.pipe gulp.dest('prd/js')
gulp.task 'buildjs', ['copy'], ->
gulp.src('src/js/**/*.js')
.pipe uglify({output:{ascii_only:true}})
.pipe gulp.dest('prd/js')
gulp.task 'zip', ['buildjs'], ->
gulp.src('prd/js/*.js')
.pipe(zip('release.zip'))
.pipe(gulp.dest('prd'))
这样顺序是没啥问题了,就是看着挺别扭的,我需要一个构建js文件夹里面内容的任务,却需要一层又一层地依赖多个任务。 还有一种办法可以把所有步骤一股脑地放在一个任务里,就是利用流的事件,vinyl的流和原生fs流其实基本一样, 也有那些事件,所以可以利用end事件来控制顺序。我认为上面的copy和buildjs两个任务不应当拆开,就把他们写在一起:
gulp.task 'copy', ['clean'], (cb) ->
lastStream = null
gulp.src(['src/js/**/*.*', '!src/js/**/*.js'])
.pipe gulp.dest('prd/js').
.on 'end', ->
lastStream = gulp.src('src/js/**/*.js')
.pipe uglify({output:{ascii_only:true}})
.pipe gulp.dest('prd/js')
.on 'end', cb
要注意的是,下一个任务需要等待这个任务最有一个流执行完再开始,所以这里需要在最后执行的流上加上对end事件的处理,执行参数传入的回调。