如何在任务失败时立即中断后续串行队列

解读

面试官真正想考察的是你对 Grunt 串行任务链(task queue) 的容错机制是否熟悉。
国内项目普遍把「编译 → 代码检查 → 单元测试 → 打包 → 上传」写成一条串行链,任何一步报错如果继续往下跑,轻则把脏代码发到预发环境,重则把线上资源覆盖掉。
能否在第一步失败就立刻退出进程,是衡量候选人是否具备「工程化安全意识」的硬指标。
回答时要同时给出配置层(Gruntfile)运行时(CLI) 两种做法,并指出 Grunt 1.0 之后默认行为的差异,才能体现“资深”。

知识点

  1. --force--no-force
    默认情况下 Grunt 1.0 已经不再自动继续,但国内很多老项目仍停留在 0.4.x,候选人必须知道老版本需要显式关闭 --force
  2. this.async() 的两种用法
    任务内部如果调用 var done = this.async();,一旦传入 done(false) 或抛异常,Grunt 核心会标记该任务失败。
  3. grunt.fail 模块
    grunt.fail.fatal() 会立即终止整个队列;grunt.warn() 可被 --force 忽略,区别要背熟。
  4. grunt.task.clearQueue()
    官方未公开但源码可见,可在任务失败回调里手动清空剩余队列,实现“秒退”。
  5. 并发任务与串行任务
    国内常把 grunt.registerTask('default', ['a', 'b', 'c']) 写成串行,并发任务(concurrent) 失败时默认不会中断主队列,需要额外监听 concurrent:done:fail 事件。
  6. CI 场景
    阿里、腾讯等厂内脚本统一加 set -e,一旦 Grunt 返回非 0 即结束;候选人要说明本地如何模拟同样行为。

答案

在 Grunt 1.0 及以上版本,默认行为就是“任务失败立即中断后续串行队列”,无需额外配置;
若项目基于 0.4.x 或有人显式加了 --force,则按以下三步保证“失败即停”:

  1. 启动命令去掉容错开关

    grunt default --no-force   # 老版本必须加
    
  2. 每个任务内部统一使用 this.async() 并正确回传状态

    grunt.registerTask('eslint', function() {
      var done = this.async();
      grunt.util.spawn({ cmd: 'npx', args: ['eslint', 'src'] }, function(err) {
        if (err) {
          grunt.log.error('ESLint 未通过');
          return done(false);   // 标记失败,Grunt 会立即中断队列
        }
        done();
      });
    });
    
  3. 若想在失败时额外做清理,可监听 grunt.event.on('fail') 并手动 grunt.task.clearQueue(),确保后续任务连日志都不打印。

一句话总结:“只要不用 --force,且任务内部正确回传失败状态,Grunt 会自动中断串行队列;老版本需显式 --no-force。”

拓展思考

  1. 如果任务链里混入了 grunt-concurrent,如何做到“子进程一失败,主进程秒退”?
    答:在 concurrent 目标里加 grunt.event.on('concurrent:fail', grunt.fail.fatal),把子进程失败事件提升到主进程 fatal 级别。
  2. 大型 Monorepo 中,不同子包用同一个 Gruntfile,如何只中断当前包而不影响其他包?
    答:把每个包写成独立的 grunt.task.run,外层用 Node 脚本循环调用,失败时通过 process.exit(1) 跳出循环,而不是在 Grunt 内部清队列。
  3. 如何在失败时把错误信息同步到企业微信或飞书?
    答:在 grunt.fail.registerReporter(function(msg){ axios.post(webhook, {text:msg}) }),利用官方 Reporter 机制,保证任何 fatal 都能推送到群,避免“本地成功、CI 失败”无人感知。