Node.js 工具链中的典型任务运行器有 Grunt ,以及后起之秀 Gulp ,现在它们都有着广泛的社区支持,都有大量的插件支持。Grunt 以其先入为主的优势,以及直观的插件加配置的方式几乎提供了一个“立即可用”的模型。与以传统的方式进行配置的 Grunt 相比, Gulp 则使用“代码”的方式来描述任务。而它们另一个巨大的不同,则体现在运行方式上:Grunt 的运行方式很直观:解析依赖,使用配置逐步运行已定义的任务;而 Gulp 则默认将所有任务和步骤异步化运行。显而易见,Gulp 在效率上是有明显的提升的。但也带来了一些概念上的转变,以及,如果我们熟悉了 Grunt 或者其他传统的编译工具,比如 Ant 或 MSBuild,会在有同步运行任务需求时遭遇难题。
因为任务是异步运行的,Gulp 便默认将并行运行所有任务;任务中的步骤也是异步的,因此各个步骤也是并行的。比如在下面的任务里,两个删除操作将会并行运行:
gulp.task('clean', function(){
gulp.src("./dist/**/*.js", { read: false }).pipe(rimraf());
gulp.src("./dist/**/*.css", { read: false }).pipe(rimraf());
});
在 Gulp 里,即使你像下面这样,将任务 release 定义为依赖另外两个任务 clean, minify,实际在运行 release 之前,clean 和 minify 仍是并行运行的。
gulp.task('clean', function(){
return gulp.src("./dist/**/*.js", { read: false }).pipe(rimraf());
});
gulp.task('minify', function(){
return gulp.src('./js/**/*.js').pipe(uglify()).pipe(gulp.dest("./dist/js"));
});
gulp.task('release', ['clean', 'minify'], function(){
// do stuff
});
在 Gulp 官方的文档 中,给出一种“通过两个步骤”的方法来实现所谓同步运行。假如我们希望 T2 在 T1 之后运行,那么:
在步骤 1 中,运行时会等待 Promise 或 Stream 完成,或者等待 callback 被调用,以确定 T1 已经完成执行,再执行 T2。
也就是说,在之前的例子中,我们需要为 minify 添加对 clean 的依赖,即:
gulp.task('clean', function(){
return gulp.src("./dist/**/*.js", { read: false }).pipe(rimraf());
});
gulp.task('minify', ['clean'], function(){
return gulp.src('./js/**/*.js').pipe(uglify()).pipe(gulp.dest("./dist/js"));
});
gulp.task('release', ['clean', 'minify'], function(){
// do stuff
});
看似已经比较完美地解决了这一问题。
可这与一开始我们的意图已经有了变化:minify 任务现在依赖了 clean 任务,而它的业务原本是不依赖 clean 的——我们只是为了“屈从 Gulp 的设计”才定义了这样的依赖关系。这种在 minify 中隐式包含 clean 的做法有时候会带来麻烦。比如,你在执行的时候,确实只是需要执行一个 minify,怎么办?那时,你就需要定义一个专门的 minify-only——为了与现有的 minify 重用代码,你需要将它逻辑提取为单独的函数,是不是感受到了一些无奈?
那么,怎样才能“优雅地”逐个同步地运行 Gulp 任务呢?
当然,问题总是有解决方案的。
国内的 Teambition 团队开源了 gulp-sequence ,以及国外开发者开发的 run-sequence 均能很好地解决这个问题。它们提供了类似的调用方式,下面的代码演示如何使用 run-sequence 按顺序地运行多个或多组 Gulp 任务:
var runSequence = require('run-sequence');
gulp.task('default', function(callback) {
runSequence('clean',
['less', 'scripts'],
'watch',
callback);
});
在上述代码中,clean 先于所有其他任务运行,在 clean 完成后,less 与 scripts 同时运行;在 less 与 scripts 都运行完成之后,watch 最后运行。并且,在 watch 运行完毕后,会调用 callback,以通知 Gulp 引擎。
一个典型的 minify 任务可能既包含对 JavaScript 文件的压缩,又包含对 css 文件的压缩;但对 JavaScript 使用的是 uglify,而对 css 的压缩则是使用的 cssmin。这时,我们写了如下的任务:
gulp.task("minify", function() {
gulp.src('./styles/**/*.css', { base: "./" })
.pipe(cssmin())
.pipe(gulp.dest('.'));
gulp.src('./scripts/**/*.js', { base: "./" })
.pipe(uglify())
.pipe(gulp.dest("."));
});
上述任务中,链式调用的 pipe 方法返回的是异步的 Stream 对象 ,如果我们需要监控整个 minify 任务的完成情况(那样,别的任务就可以依赖 minify 了),就比较麻烦了。如果任务里只有单纯的一个 pipe 链式步骤,那我们将其直接作为返回值即可。而现在我们有两个异步的 Stream,理论上,这两个 Stream 都完成时,整个 minify 任务就完成了。
这时,我们可以使用 merge-stream 来“合并”多个 Stream,使其再次变为单个 Stream,再以它为返回值——这样便解决了问题。在做了很简单的修改之后,实现代码如下:
var mergeStream = require('merge-stream');
gulp.task("minify", function() {
var mincss = gulp.src('./styles/**/*.css', { base: "./" })
.pipe(cssmin())
.pipe(gulp.dest('.'));
var minjs = gulp.src('./scripts/**/*.js', { base: "./" })
.pipe(uglify())
.pipe(gulp.dest("."));
return mergeStream(mincss, minjs);
});
在 Gulp 的世界里,“在单个任务同步地运行多个步骤” 这个想法是不是觉得有些疯狂?即使在普通 node.js 应用程序里,这样的想法都会是比较格格不入的。不过,我不喜欢被束住手脚的感觉,虽然我可以使用上面的“以同步的方式运行多个任务”那样来完成工作,但我仍偏执地认为一些小的步骤应该被放置在单个任务里。
例如一个典型的样式表编译工作,我希望 scss 文件编译为 css 文件之后,自动生成对应的压缩版本。我可以这样编写 Gulp 脚本:
gulp.task('scss', function() {
return gulp.src('./scss/**/*.scss', { base: "./" })
.pipe(sass())
.pipe(gulp.dest('./styles'));
});
gulp.task('cssmin', ['scss'], function(){
return gulp.src('./styles/**/*.css', { base: "./" })
.pipe(cssmin())
.pipe(gulp.dest('.'));
});
这样,每次执行 cssmin 任务即可完成所有工作,一切看起来很美好。直到我的样式表编译工作需要在 cssmin 之后需要增加一个新的步骤:将所有的 css 文件合并成单个文件。这时候,我需要在 cssmin 的任务之后再增加一个 css-concat 来完成工作。所有这一切,只是为了这样一个 “样式表编译工作”。为什么我不能用单个的 compile-css 来包含这所有的步骤,并且在将来任何时候再愉快地增加新的步骤呢?
后来经过观察发现,我几乎从来不会专门去调用 scss,或者 cssmin,我只会直接调用最终的 css-concat——这再一次为“单个的 compile-css 任务”的需求提供了佐证。
于是,我动手解决了它,发布了 gulp-awaitable-tasks 这样一个 npm 包。有了它,我们可以这样编写 Gulp 任务,注意声明时匿名函数上的星号(*)及函数内部的 yield 关键字:
require('gulp-awaitable-tasks')(gulp);
gulp.task('compile-css', function*() {
yield gulp.src('./scss/**/*.scss', { base: "./" })
.pipe(sass())
.pipe(gulp.dest('./styles'));
yield gulp.src('./styles/**/*.css', { base: "./" })
.pipe(cssmin())
.pipe(gulp.dest('.'));
});
至于原理,就像能够使用Generator 函数来解决 异步回调大坑(callback hell) 的做法一样,Generator 函数在此处也可以帮到我们。只有不到 50 行代码,请自行参考。
Gulp 本身已是一个优秀的任务组织工具,再加上基于它有大量插件,绝大多数的前端任务它已经能够帮助我们很多。不过工具要用的称心如意,还是需要磨合的。希望此文章介绍的技巧和工具能够帮助您提高效率。
文中所提到的 npm 包: