让你的 gruntfile 变强

如何充分利用 build 配置

Paul Bakaus
Paul Bakaus

简介

如果你对 Grunt 世界还不熟悉,不妨从 Chris Coyier 的优秀文章“Grunt for People Who Think Like Grunt are Weird and Hard”(研究 Grunt 的思考方式与 Grunt 十分怪异和难以理解)的文章着手。在 Chris 的介绍之后,你将设置自己的 Grunt 项目,并体验一下 Grunt 的强大功能。

在本文中,我们不会关注众多 Grunt 插件对实际项目代码的作用,而是关注 Grunt 构建流程本身。我们将为您提供以下方面的实用建议:

  • 如何让 Gruntfile 保持整洁有序?
  • 如何大幅缩短构建时间
  • 以及如何在进行构建时收到通知。

该快速免责声明:Grunt 只是您用来完成这项任务的众多工具之一。如果 Gulp 更符合你的风格,那就太好了!如果在调查各种选项后,您仍然希望构建自己的工具链,也没有关系!在撰写本文时,我们选择将重点放在 Grunt 上,因为 Grunt 有强大的生态系统和历史悠久的用户群。

整理 Gruntfile

无论您是包含大量 Grunt 插件,还是需要在 Gruntfile 中编写大量手动任务,它都可能很快变得非常繁琐且难以维护。幸运的是,有不少插件正好可以解决这个问题:让 Gruntfile 再次整洁有序。

优化前的 Gruntfile

下面是我们进行任何优化之前的 Gruntfile:

module.exports = function(grunt) {

  grunt.initConfig({
    pkg: grunt.file.readJSON('package.json'),
    concat: {
      dist: {
        src: ['src/js/jquery.js','src/js/intro.js', 'src/js/main.js', 'src/js/outro.js'],
        dest: 'dist/build.js',
      }
    },
    uglify: {
      dist: {
        files: {
          'dist/build.min.js': ['dist/build.js']
        }
      }
    },
    imagemin: {
      options: {
        cache: false
      },

      dist: {
        files: [{
          expand: true,
          cwd: 'src/',
          src: ['**/*.{png,jpg,gif}'],
          dest: 'dist/'
        }]
      }
    }
  });

  grunt.loadNpmTasks('grunt-contrib-concat');
  grunt.loadNpmTasks('grunt-contrib-uglify');
  grunt.loadNpmTasks('grunt-contrib-imagemin');

  grunt.registerTask('default', ['concat', 'uglify', 'imagemin']);

};

如果您现在说“嘿!我原本以为会更糟!这其实是可以维护的!”,你可能是对的。为简单起见,我们仅包含三个插件,没有进行太多自定义。在本文中,如果使用实际生产 Gruntfile 构建中等大小的项目,则需要无限滚动。让我们看看能做些什么!

自动加载 Grunt 插件

将想要使用的新 Grunt 插件添加到项目中时,您必须将其作为 npm 依赖项同时添加到 package.json 文件中,然后在 Gruntfile 中加载它。对于插件“grunt-contrib-concat”,创建记录可能如下所示:

// tell Grunt to load that plugin
grunt.loadNpmTasks('grunt-contrib-concat');

如果您现在通过 npm 卸载插件并更新 package.json,但忘记更新 Gruntfile,您的构建将会中断。这正是强大的插件 load-grunt-tasks 的用武之地。

而以前,我们必须手动加载 Grunt 插件,如下所示:

grunt.loadNpmTasks('grunt-contrib-concat');
grunt.loadNpmTasks('grunt-contrib-uglify');
grunt.loadNpmTasks('grunt-contrib-imagemin');

借助 load-grunt-tasks,您可以将其收起为下面的一行代码:

require('load-grunt-tasks')(grunt);

请求该插件后,它会分析您的 package.json 文件,确定哪些依赖项是 Grunt 插件,然后自动加载它们。

将 Grunt 配置拆分为不同的文件

load-grunt-tasks 在代码方面和复杂性略有缩减 Gruntfile,但是当您配置大型应用时,它仍然会变成非常大的文件。这正是 load-grunt-config 的用武之地。load-grunt-config 让你能按任务拆分 Gruntfile 配置。此外,它还封装了 load-grunt-tasks 及其功能!

但重要提示:拆分 Gruntfile 文件可能并不总是适合所有情况。如果您的任务之间存在大量共享配置(即使用大量 Grunt 模板),则应格外小心。

使用 load-grunt-config 时,Gruntfile.js 将如下所示:

module.exports = function(grunt) {
  require('load-grunt-config')(grunt);
};

没错,就是全部内容!我们的任务配置现在位于何处?

在 Gruntfile 的目录中创建一个名为 grunt/ 的文件夹。默认情况下,插件会包含该文件夹中与您要使用的任务名称匹配的文件。我们的目录结构应如下所示:

- myproject/
-- Gruntfile.js
-- grunt/
--- concat.js
--- uglify.js
--- imagemin.js

现在,我们将每个任务的任务配置直接放入相应的文件中(您会发现,这些文件大多只是从原始 Gruntfile 中复制和粘贴到新结构中):

grunt/concat.js

module.exports = {
  dist: {
    src: ['src/js/jquery.js', 'src/js/intro.js', 'src/js/main.js', 'src/js/outro.js'],
    dest: 'dist/build.js',
  }
};

grunt/uglify.js

module.exports = {
  dist: {
    files: {
      'dist/build.min.js': ['dist/build.js']
    }
  }
};

grunt/imagemin.js

module.exports = {
  options: {
    cache: false
  },

  dist: {
    files: [{
      expand: true,
      cwd: 'src/',
      src: ['**/*.{png,jpg,gif}'],
      dest: 'dist/'
    }]
  }
};

如果 JavaScript 配置块并不是您的真正需要,load-grunt-tasks 甚至允许您改用 YAML 或 CoffeeScript 语法。我们在 YAML 中编写最终的必需文件,即“aliases”文件。这是一个用于注册任务别名的特殊文件,我们之前必须通过 registerTask 函数在 Gruntfile 中执行此操作。以下是我们的活动:

grunt/aliases.yaml

default:
  - 'concat'
  - 'uglify'
  - 'imagemin'

这样就大功告成了!在终端中执行以下命令:

$ grunt

如果一切正常,此操作会查看“默认”任务,并按顺序运行所有内容。现在,我们已将主 Gruntfile 精简为三行代码,无需再处理和外部化每个任务配置,至此,我们就完成了。但是,建立所有东西仍然很慢。让我们来看看可以采取哪些措施来改进这一点。

最大限度地缩短构建时间

尽管与执行构建所需的时间相比,Web 应用的运行时和加载时间性能对业务而言更为重要,但构建缓慢仍然会带来问题。这会使使用 grunt-contrib-watch 等插件或在 Git 提交足够快后执行自动构建变得非常困难,并且会造成实际运行构建的“惩罚”:构建时间越短,工作流就越敏捷。如果生产 build 的运行时间超过 10 分钟,您就只在绝对必要时才运行 build,而且在它运行时会走到路上喝杯咖啡。这是工作效率的杀手。我们还有一些改进措施。

仅实际更改的构建文件:grunt-newer

网站完成初始构建后,当您再次构建时可能只触及了项目中的几个文件。假设在我们的示例中,您更改了 src/img/ 目录中的一张图片。运行 imagemin 以重新优化图片是合理的,但仅针对这一张图片。当然,重新运行 concatuglify 只是在浪费宝贵的 CPU 周期。

当然,您始终可以从终端(而不是 $ grunt)运行 $ grunt imagemin,以便仅选择性地执行当前任务,但还有一种更智能的方法。叫做 grunt-newer

Grunt-newer 具有本地缓存,其中存储有关实际更改文件的信息,并且仅针对确实已更改的文件执行任务。我们来看看如何启用它。

还记得我们的 aliases.yaml 文件吗?将其更改为:

default:
  - 'concat'
  - 'uglify'
  - 'imagemin'

更改为:

default:
  - 'newer:concat'
  - 'newer:uglify'
  - 'newer:imagemin'

只需在任何任务前面附加“newer:”,就会首先通过 grunt-newer 插件传送源文件和目标文件,然后该插件会确定应针对哪些文件(如果有)运行

并行运行多个任务:grunt-concurrent

当大量任务相互独立且需要消耗大量时间时,grunt-concurrent 这个插件会非常有用。它利用设备中的 CPU 数量并并行执行多个任务。

最棒的是,它的配置超级简单。假设您使用 load-grunt-config,请创建以下新文件:

grunt/concurrent.js

module.exports = {
  first: ['concat'],
  second: ['uglify', 'imagemin']
};

我们刚刚设置了名为“first”和“second”的并行执行轨道。在我们的示例中,concat 任务需要先运行,在此期间无需运行任何其他任务。我们在第二个轨道中放入了 uglifyimagemin,因为它们是相互独立的,并且都需要花费大量时间。

这本身不会起到任何作用。我们必须更改 default 任务别名,使其指向并发作业而不是直接作业。以下是 grunt/aliases.yaml 的新内容:

default:
  - 'concurrent:first'
  - 'concurrent:second'

如果您现在重新运行 grunt 构建,并发插件将首先运行 concat 任务,然后在两个不同的 CPU 核心上生成两个线程,以并行运行 imagemin 和 uglify。棒极了!

不过,有一点建议:在我们的基本示例中,grunt-concurrent 可能并不能显著加快构建速度。原因在于,在不同线程中生成不同的 Grunt 实例会产生开销:在我的示例中,专业生成时间至少 +300 毫秒。

花了多长时间? 时间不满

既然我们正在优化每项任务,了解每项任务需要执行多长时间会非常有帮助。幸运的是,还有一个插件可以做到:time-grunt

time-grunt 不是作为 npm 任务加载的经典 grunt 插件,而是直接包含的插件,类似于 load-grunt-config。我们将向 Gruntfile 添加对 time-grunt 的要求,就像对 load-grunt-config 添加的要求一样。我们的 Gruntfile 现在应如下所示:

module.exports = function(grunt) {

  // measures the time each task takes
  require('time-grunt')(grunt);

  // load grunt config
  require('load-grunt-config')(grunt);

};

很抱歉令你感到失望,今天就结束了 - 尝试从终端重新运行 Grunt,对于每项任务(以及整个 build),您应该会看到一个格式非常规范的信息面板,其中包含了执行时间:

古怪时间

自动系统通知

现在,您已经拥有一个经过充分优化的 Grunt 构建,可以快速执行,并以某种方式自动构建它(例如,通过使用 grunt-contrib-watch,或在提交后进行),如果您的系统可以在您的新版本可供使用时或发生任何问题时通知您,那该有多好?Meet grunt-notify

默认情况下,grunt-notify 会使用你的操作系统上可用的任何通知系统来提供自动通知,包括:Growl for OS X 或 Windows、Mountain Lion’s 和 Mavericks 通知中心以及通知发送。令人惊讶的是,要想获得此功能,您只需从 npm 安装插件并将其加载到 Gruntfile 中(请注意,如果您使用的是上面的 grunt-load-config,此步骤是自动完成的!)。

该示例取决于您的操作系统,具体如下所示:

通知

除了错误和警告之外,我们还需要对其进行配置,使其在上一个任务执行完毕后运行。假设您使用 grunt-load-config 将任务拆分到多个文件中,我们需要用到此文件:

grunt/notify.js

module.exports = {
  imagemin: {
    options: {
      title: 'Build complete',  // optional
        message: '<%= pkg.name %> build finished successfully.' //required
      }
    }
  }
}

在我们的配置对象的第一级别,密钥必须与我们要关联的任务的名称一致。在此示例中,该消息会在 imagemin 任务执行(即构建链中的最后一个任务)后立即显示。

全部总结

如果您能从高处开始构建流程,现在您已经非常自豪地构建流程。该流程超级简洁、井然有序,由于并行化和选择性处理,而且速度超快,并会在出现问题时通知您。

如果你发现了可进一步改进 Grunt 及其插件的其他工具,请告诉我们!祝您愉快!

更新(2014 年 2 月 14 日):如需获取完整的工作示例 Grunt 项目的副本,请点击此处