Grunt vs Gulp

最近一直在尝试一些新的模块化工具,发现了 gulp 比较好用,但是之前使用过一段时间的grunt,所以根据以下几个要点来将一席grunt和gulp使用的区别,侧重讲以下在使用gulp过程中发现的问题。

1. 书写方式

grunt 运用配置的思想来写打包脚本,一切皆配置,所以会出现比较多的配置项,诸如option,src,dest等等。而且不同的插件可能会有自己扩展字段,导致认知成本的提高,运用的时候要搞懂各种插件的配置规则。
gulp 是用代码方式来写打包脚本,并且代码采用流式的写法,只抽象出了gulp.src, gulp.pipe, gulp.dest, gulp.watch 接口,运用相当简单。经尝试,使用gulp的代码量能比grunt少一半左右。

2. 任务划分

grunt 中每个任务对应一个最外层配置的key, 大任务可以包含小任务,以一种树形结构存在。举个栗子:

uglify: { 
  one: { 
    src: 'src/a.js', 
    dest: 'dest/a.min.js' }, 
    two: { 
      src: 'tmp/b.js', 
      dest: 'dist/b.min.js' 
    }
}

将uglify划分子任务的好处是,我们在封装不同的task时可以分别对'uglify:one'或'uglify:two'进行调用,这对于某些需要在不同时间点调用到uglify的task相当有用。

gulp 中没有子任务的概念,对于上面的需求,只能通过注册两个task来完成

gulp.task('uglify:one', function(){ 
  gulp.src('src/a.js') 
      .pipe(uglify()) 
      .dest('dest/a.min.js')});
gulp.task('uglify:two', function(){ 
  gulp.src('tmp/b.js')
     .pipe(uglify())
     .dest('dist/b.min.js')
});

当然这种需求往往可以通过调整打包策略来优化,并不需要分解子task,特殊情况下可以用这种方法解决。

3. 运行效率

grunt 采用串行的方式执行任务,比如我们注册了这样一个任务:

grunt.register('default', ['concat', 'uglify', 'release'])

grunt 是按书写的顺序首先执行cancat,然后是uglify,最后才是release,一派和谐的气氛,谁也不招惹谁。而我们知道某些操作时可以 同步执行的,比如cssmin和uglifyjs。这时grunt无法通过简单地更改配置来达到并行执行的效果,通常的做法是手动写异步task,举个例子:

grunt.registerTask('cssmin', 'async cssmin task', function() { 
  var done = this.async(); cssmin(done);
});

在cssmin操作完成后传入done方法告知程序,但这需要插件支持。
gulp 基于并行执行任务的思想,通过一个pipe方法,以数据流的方式处理打包任务,我们来看这段代码:

gulp.task('jsmin', function () { 
  gulp.src(['build/js/**/*.js']) 
    .pipe(concat('app.min.js')) 
    .pipe(uglify() 
    .pipe(gulp.dest('dist/js/'));
});

程序首先将build/js下的js文件压缩为app.min.js, 再进行uglify操作,最后放置于dist/js下。这一系列工作就在一个task中完成,中间没有产生任何临时文件。如果用grunt,我们需要怎样 写这个任务?那必须是有两个task配置,一个concat,一个uglify,中间还必须产生一个临时文件。从这个角度来说,gulp快在中间文件的产 生只生成于内存,不会产生多余的io操作。
再来看看前面的问题,如何并行执行uglify和cssmin?其实gulp本身就是并发执行的,我们并不需要多什么多余多工作,只需

gulp.task('default', ['uglify', 'cssmin']);

gulp 该怎么快就怎么来,并不会等到uglify再执行cssmin。是不是觉得gulp秒杀grunt几条街了呢?且慢,坑还在后面...首先我们需要问一个问题,为什么要用并发?为了快?那什么时候可以快,什么时候又不能快?假设我们有这样一个任务:

gulp.task('jsmin', ['clean', 'concat']);

需要先将文件夹清空,再进行合并压缩,根据gulp的并发执行的方式,两个任务会同时执行,虽然从指令上看是先执行了clean再执行concat,然而clean还没结束,concat就执行了,导致代码合并了一些未被清理的文件,这显然不是我们想要的结果。

那这个问题有没有什么解决方案呢?

gulp 官方API给出了这样的方法:给出一个提示,来告知 task 什么时候执行完毕并且再给出一个提示,来告知一个 task 依赖另一个 task 的完成

官方举了这个例子:

让我们先假定你有两个 task,"one" 和 "two",并且你希望它们按照这个顺序执行:

  1. 在 "one" 中,你加入一个提示,来告知什么时候它会完成:可以再完成时候返回一个 callback,或者返回一个 promise 或 stream,这样系统会去等待它完成。
  2. 在 "two" 中,你需要添加一个提示来告诉系统它需要依赖第一个 task 完成。
    因此,这个例子的实际代码将会是这样:
var gulp = require('gulp');
// 返回一个 callback,因此系统可以知道它什么时候完成
gulp.task('one', function(cb) { 
    // 做一些事 -- 异步的或者其他的 cb(err); 
    // 如果 err 不是 null 或 undefined,则会停止执行,且注意,这样代表执行失败了
});
// 定义一个所依赖的 task 必须在这个 task 执行之前完成
gulp.task('two', ['one'], function() { 
    // 'one' 完成后
});
gulp.task('default', ['one', 'two']);

task one 执行完毕后需要调用cb方法来告知task two我已经执行完成了,你可以干你的事了。
那在我们实际运用中,通常是这样的:

gulp.task('clean', function (cb) { gulp.src(['tmp']) .pipe(clean());});

这个时候clean结束的cb要写在哪呢?是这样吗?

gulp.task('clean', function (cb) { gulp.src(['tmp']) .pipe(clean()); cb();});

对于理解什么叫异步的人来说这种方法肯定是不行的,clean还没完成,cb已经执行了。好在!!!
好在我们可以利用gulp中的时间监听来做结束判断:

gulp.task('clean', function (cb) { 
  gulp.src(['tmp'])
    .pipe(clean())
    .on('end', cb);
});
gulp.task('concat', [clean], function(){ 
  gulp.src('blabla') 
    .pipe('blabla') 
    .dest('blabla');
});

由于 gulp 是用node实现的,所以必然绑定了数据流的监听事件,我们通过监听stream event end来达到这个目的。
而不得不吐槽的是通过在task后面写[]依赖的方式也并不优雅,通常可以通过其他插件来达到顺序执行的效果,写法如同grunt,但是每个task的end事件的监听也是少不了的。
如果你的任务不多的时候,直接在回调后面执行concat也是可以的:

gulp.task('clean', function(){});
gulp.task('concat', function(){});
gulp.task('clean-concat', ['clean'], function(){ 
      gulp.start('concat');
});

4. 其他要交代的

gulp 真的只有src, pipe, dest, watch, run这几个API吗? 不,由于gulp继承了Orchestrator(<4.0),所以具备了另外一些API,包括start等。当然这些API是官方不推荐使用的。会导致代码的复杂度提升,所以并没有出现在官方文档中。
不建议将多个操作写在同个task中,这样程序并不知道任务及时结束,如:

gulp.task('test', function(cb) { 
  gulp.src('bootstrap/js/*.js') 
    .pipe(gulp.dest('public/bootstrap')) 
    .on('end', cb); 
  gulp.src('jquery.cookie/jquery.cookie.js') 
    .pipe(gulp.dest('public/jquery')) 
    .on('end', cb);
});

尽量减少task的数量,很多任务其实可以在一个task中用多个pipe来执行,只需要我们在打包等时候规划好文件夹及任务流。

对了,gulp4.0会带给我们很多惊喜(wtf!),虽然它还是迟迟未发布... 暂时不想去踩坑。读者可自行Google。

原文链接:https://github.com/fwon/blog/issues/16

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,117评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,328评论 1 293
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,839评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,007评论 0 206
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,384评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,629评论 1 219
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,880评论 2 313
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,593评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,313评论 1 243
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,575评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,066评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,392评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,052评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,082评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,844评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,662评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,575评论 2 270

推荐阅读更多精彩内容