w3ctech

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-usemingulp-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不需要有minifyCssminifyHtmluglifyrev这几个插件的任何知识,只要把对应的内容从stream丢出去就好。在用这些插件组装task时才需要关心。

当前,Gulp的社区还远不如Grunt成熟,有些功能的插件,Gulp可能就没有。这其实不算很大的劣势,只要足够好用,追上来很快。而且,写一个Gulp插件要比相应的Grunt插件短小得多!

[原作发表于简书]

w3ctech微信

扫码关注w3ctech微信公众号

共收到5条回复

  • 赞一个!

    回复此楼
  • 正在用。。,之前一直没用过grunt

    回复此楼
  • 表示在用grunt, 我倒是感觉gulp有点复杂了, grunt也可以写成自己定义的东东, 然后读配置.json啊,挺方便, 谁都有谁的优点, 不一定厉害的对自己就好, 感觉适合的才是真爱...

    回复此楼
  • 赞~生平最怕就是配置

    回复此楼
  • 很久没有配置Gruntfile了,今天想拓展以前写的Gruntfile的时候竟然无从下手……所以怒转Gulp

    回复此楼