Gulp挑战Grunt,背后的哲学
[按:网上介绍Gulp和Grunt安装使用的文章很多,甚少比较二者的思路,连官方文档都语焉不详。我在此做一个粗陋的对比,希望能提纲挈领,加深读者对这两个工具的理解。]
做过点儿正经开发的同学都知道,构建工具必不可少。C时代的Make、Java的Ant、Ruby的Rake……没有这些工具,一遍遍地点选输入,准烦死你。
在前端和Node JS的开发中,最普及的构建工具就是Grunt。它的功能说来简单,就是管理一系列的Task。大部分的Task都是第三方的插件,安装好对应的NPM包,再loadNpmTasks
就可以用了。
Grunt的配置文件Gruntfile
,主要包含两部分:
-
配置每个Task,包括文件从哪里,到哪里去,还有一些处理的选项
-
自己写一些简单的Task,把第三方插件提供的Task组合起来
别看这两个事儿,轻轻松松几百行出来了。每个Task的配置,各有各的规矩,还牵扯到插件间的配合。反正我从seed库开始做新项目的时候,基本不敢改原来的Gruntfile,很多用不上的功能也搁那儿。留意了一下很多开源项目的Gruntfile,也都臃肿杂乱,好不到哪儿去。
Gruntfile维护起来那么困难,有几个原因:
-
配置和运行分离
程序员都知道,变量的声明和使用挨在一起,最方便理解和修改。但Gruntfile里,配置Task和调用它们的地方离得很远,极大地增加了心智负担。 -
每个插件做的事太多
每个Task的结果必须写到磁盘文件,另一个Task再读,损害性能倒是小事,更麻烦的是让整个过程变复杂了。
就像一个个小作坊,来料加工又返回给客户,这中间的沟通成本、出错机会都大大增加。 -
配置项过多
做事多了,配置项自然也多。至少输入和输出的位置得配吧。每个插件的配置规则还不尽相同。用每个插件,都得去学习一番。
Gulp应运而生。
恐怕没几个IT人不知道Unix管道的概念。前一级的输出,直接变成后一级的输入。把简单的工具组合起来,优雅地解决复杂的问题。听起来那么熟悉呢?是的,Gulp就把这种思维用在构建过程中。
Gulp基于Node JS的一个机制,叫做stream,有点类似C++中的stream。在Node中,文件访问、输入输出、HTTP连接,都是stream。Gulp的每个插件从stream中读取输入,做一些处理,再输出到stream中。
每个插件不是拿来独立使用的。相反,它专注于完成单一职责。只有把合适的插件组合起来,才能完成具体的Task。引用官方的例子,看看一个典型的Task长什么样(略有删减):
var paths = {
scripts: ['client/js/**/*.coffee', '!client/external/**/*.coffee']
};
gulp.task('scripts', ['clean'], function() { // 可以依赖于其它task
return gulp.src(paths.scripts) // 指定输入
.pipe(coffee()) // 环节一
.pipe(uglify()) // 环节二
.pipe(concat('all.min.js')) // 环节三
.pipe(gulp.dest('build/js')); // 指定输出
});
配置呢?不需要了。是不是行云流水,一气呵成?
那我们再回头来看看前面Grunt的几个问题,Gulp是怎么解决的:
-
配置和运行分离
code over configuration,直接就在调用的地方配置。 -
每个插件做的事太多
单一职责,依靠组合来发挥作用。就像一条自动化生产线,上一道工序的产出直接交给下一步,效率不要太高。 -
配置项过多
既然大家都遵循同一个协议,很多配置就不需要了。
放大了看,Gulp像是一个非常贴近领域模型的DSL,而Grunt更像万能的XML。哪个好用,无需多说。在我们制作DSL时,也有参考意义。
最后,举一个Grunt很别扭,Gulp却能优雅解决的例子。
做前端开发会用到一个功能叫usemin
。我们HTML中会引用到很多css和js文件。发布时,这些文件要合并、压缩、混淆,最后生成一两个文件。为了让修改过的代码绕过浏览器的缓存机制,要根据文件内容hash出文件名。html文件里就要引用这些新的文件名。
比较一下grunt-usemin和gulp-usemin各自README的长度,就能看出区别。
grunt.registerTask('build', [
'useminPrepare', // 准备
'concat',
'cssmin',
'uglify',
'filerev',
'usemin' // 执行
]);
grunt-usemin
分成两步:
-
先从html文件中收集需要处理的js和css,传给后续的一堆任务
它本身并不知道在实际中会调用哪些其它Task,只能用一些hack,支持固定的几个Task。而上面的每个Task,都有自己的配置项。要把这些配置项都列出来,实在太长了。 -
真正执行,更新html文件里的js和css引用。
而gulp-usemin
就干净得多,没有丝毫多余的东西:
gulp.task('usemin', function() {
gulp.src('./*.html')
.pipe(usemin({
css: [minifyCss(), 'concat'],
html: [minifyHtml({empty: true})],
js: [uglify(), rev()]
}))
.pipe(gulp.dest('build/'));
});
usemin
不需要有minifyCss
、minifyHtml
、uglify
和rev
这几个插件的任何知识,只要把对应的内容从stream丢出去就好。在用这些插件组装task时才需要关心。
当前,Gulp的社区还远不如Grunt成熟,有些功能的插件,Gulp可能就没有。这其实不算很大的劣势,只要足够好用,追上来很快。而且,写一个Gulp插件要比相应的Grunt插件短小得多!