让 Grunt 任务接受 Marie Kondo 的整理之道

Avatar of Serj Lavrin
Serj Lavrin

DigitalOcean 提供适合您旅程各个阶段的云产品。立即开始使用 $200 免费积分!

我们生活在 webpacknpm 脚本 的时代。无论好坏,它们都成为了打包和任务运行的主力,以及一些 RollupJSPMGulp 的片段。但让我们面对现实。您的一些旧项目仍在使用老式的 Grunt。虽然它不再像以前那样闪耀,但它做得很好,所以没有太多理由去触碰它。

不过,有时您会想知道是否有一种方法可以让这些项目变得更好,对吧?那么,从 “组织您的 Grunt 任务” 文章开始,然后回来。我会等你的。这将为本文奠定基础,然后我们将共同努力,创建一个可靠的 Grunt 任务组织体系。

自动速度守护进程任务加载

为每个任务编写加载声明并不有趣,例如这样

grunt.loadNpmTasks('grunt-contrib-clean')
grunt.loadNpmTasks('grunt-contrib-watch')
grunt.loadNpmTasks('grunt-csso')
grunt.loadNpmTasks('grunt-postcss')
grunt.loadNpmTasks('grunt-sass')
grunt.loadNpmTasks('grunt-uncss')

grunt.initConfig({})

通常使用 load-grunt-tasks 来自动加载所有任务。但如果我告诉你,有一种更快的速度呢?

试试 jit-grunt!它与 load-grunt-tasks 类似,但比原生 grunt.loadNpmTasks 速度更快。

差异可能很明显,尤其是在代码库很大的项目中。

没有 jit-grunt

loading tasks     5.7s  ▇▇▇▇▇▇▇▇ 84%
assemble:compile  1.1s  ▇▇ 16%
Total 6.8s

使用 jit-grunt

loading tasks     111ms  ▇ 8%
loading assemble  221ms  ▇▇ 16%
assemble:compile   1.1s  ▇▇▇▇▇▇▇▇ 77%
Total 1.4s

1.4 秒并不像速度守护进程… 所以我在某种程度上撒了谎。但它仍然比传统方式快 6 倍!如果您好奇这是如何实现的,请阅读有关 最初问题 的信息,该问题导致了 jit-grunt 的创建。

jit-grunt 如何使用?首先,安装

npm install jit-grunt --save

然后用一行代码替换所有任务加载语句

module.exports = function (grunt) {
  // Intead of this:
  // grunt.loadNpmTasks('grunt-contrib-clean')
  // grunt.loadNpmTasks('grunt-contrib-watch')
  // grunt.loadNpmTasks('grunt-csso')
  // grunt.loadNpmTasks('grunt-postcss')
  // grunt.loadNpmTasks('grunt-sass')
  // grunt.loadNpmTasks('grunt-uncss')

  // Or instead of this, if you've used `load-grunt-tasks`
  // require('load-grunt-tasks')(grunt, {
  //   scope: ['devDependencies', 'dependencies'] 
  // })

  // Use this:
  require('jit-grunt')(grunt)

  grunt.initConfig({})
}

完成!

更好的配置文件加载

在最后一个示例中,我们告诉 Grunt 如何自行加载任务,但我们并没有完全完成工作。如 “组织您的 Grunt 任务” 所建议的,我们在这里尝试做的最有用的事情之一就是将单一的 Gruntfile 拆分为较小的独立文件。

如果您阅读了上面提到的文章,您就会知道最好将所有任务配置移动到外部文件。因此,与其使用一个大的 gruntfile.js 文件

module.exports = function (grunt) {
  require('jit-grunt')(grunt)

  grunt.initConfig({
    clean: {/* task configuration goes here */},
    watch: {/* task configuration goes here */},
    csso: {/* task configuration goes here */},
    postcss: {/* task configuration goes here */},
    sass: {/* task configuration goes here */},
    uncss: {/* task configuration goes here */}
  })
}

我们想要这样

tasks
  ├─ postcss.js
  ├─ concat.js
  ├─ cssmin.js
  ├─ jshint.js
  ├─ jsvalidate.js
  ├─ uglify.js
  ├─ watch.js
  └─ sass.js
gruntfile.js

但这将迫使我们手动将每个外部配置加载到 gruntfile.js 中,这需要时间!我们需要一种方法来自动加载我们的配置文件。

我们将使用 load-grunt-configs 来实现此目的。它接收一个路径,获取所有位于此路径下的配置文件,并为我们提供一个合并后的配置对象,我们将其用于 Grunt 配置初始化。

以下是它的工作原理

module.exports = function (grunt) {
  require('jit-grunt')(grunt)

  const configs = require('load-grunt-configs')(grunt, {
    config: { src: 'tasks/.js' }
  })

  grunt.initConfig(configs)
  grunt.registerTask('default', ['cssmin'])
}

Grunt 本身也可以做到同样的事情!看看 grunt.task.loadTasks(或它的别名 grunt.loadTasks)。

使用方法如下

module.exports = function (grunt) {
  require('jit-grunt')(grunt)

  grunt.initConfig({})

  // Load all your external configs.
  // It's important to use it _after_ Grunt config has been initialized,
  // otherwise it will have nothing to work with.
  grunt.loadTasks('tasks')

  grunt.registerTask('default', ['cssmin'])
}

Grunt 将自动加载指定目录中的所有 jscoffee 配置文件。干净利落!但是,如果您尝试使用它,您会发现它什么也没做。这是怎么回事呢?我们还需要做一件事情。

让我们再次查看 gruntfile.js 代码,这次不带注释

module.exports = function (grunt) {
  require('jit-grunt')(grunt)

  grunt.initConfig({})

  grunt.loadTasks('tasks')

  grunt.registerTask('default', ['cssmin'])
}

请注意,grunt.loadTaskstasks 目录加载文件,但从未将其分配给我们的实际 Grunt 配置。

将其与 load-grunt-configs 的工作方式进行比较

module.exports = function (grunt) {
  require('jit-grunt')(grunt)

  // 1. Load configs
  const configs = require('load-grunt-configs')(grunt, {
    config: { src: 'tasks/.js' }
  })

  // 2. Assign configs
  grunt.initConfig(configs)

  grunt.registerTask('default', ['cssmin'])
}

我们在实际加载任务配置之前初始化我们的 Grunt 配置。如果您强烈的认为这将导致我们最终得到一个空的 Grunt 配置,那么您是完全正确的。您会发现,与 load-grunt-configs 不同,grunt.loadTasks 只是将文件导入到 gruntfile.js 中。它不会做更多的事情。

哇!那么,我们如何利用它呢?让我们探索一下!

首先,在 tasks 目录中创建一个名为 test.js 的文件

module.exports = function () {
  console.log("Hi! I'm an external task and I'm taking precious space in your console!")
}

现在让我们运行 Grunt

$ grunt

我们将看到打印到控制台的内容

> Hi! I'm an external task and I'm taking precious space in your console!

因此,在导入 grunt.loadTasks 后,每个函数都在加载文件时被执行。这很好,但对我们有什么用呢?我们仍然无法做我们真正想做的事情——配置我们的任务。

等等,因为有一种方法可以从外部配置文件中命令 Grunt!在导入时使用 grunt.loadTasks 会将当前 Grunt 实例作为函数的第一个参数传递,并将它绑定到 this 上。

因此,我们可以更新我们的 Gruntfile

module.exports = function (grunt) {
  require('jit-grunt')(grunt)

  grunt.initConfig({
    // Add some value to work with
    testingValue: 123
  })

  grunt.loadTasks('tasks')

  grunt.registerTask('default', ['cssmin'])
}

… 并更改外部配置文件 tasks/test.js

// Add `grunt` as first function argument
module.exports = function (grunt) {
  // Now, use Grunt methods on `grunt` instance
  grunt.log.error('I am a Grunt error!')

  // Or use them on `this` which does the same
  this.log.error('I am a Grunt error too, from the same instance, but from `this`!')

  const config = grunt.config.get()

  grunt.log.ok('And here goes current config:')
  grunt.log.ok(config)
}

现在,让我们再次运行 Grunt

$ grunt

我们将得到什么

> I am Grunt error!
> I am Grunt error too, from the same instance, but from `this`!
> And here goes current config:
> {
    testingValue: 123
  }

看看我们如何在外部文件中访问原生 Grunt 方法,甚至能够检索当前 Grunt 配置?您是否也在考虑这一点?是的,Grunt 的全部功能都在那里,在我们每个文件中的指尖!

如果您想知道为什么外部文件中的方法可以影响我们主要的 Grunt 实例,那是因为存在引用grunt.loadTasksthisgrunt 传递给我们的当前 Grunt 实例——而不是它的副本。通过调用该引用上的方法,我们能够读取和修改我们主要的 Grunt 配置文件。

现在,我们需要实际配置一些内容!最后一件事情…

这次,让我们让配置加载真正生效

好吧,我们已经走了很长一段路。我们的任务是自动加载的,而且速度更快。我们学习了如何使用原生的 Grunt 方法加载外部配置。但是我们的任务配置还没有到位,因为它们最终并没有出现在 Grunt 配置中。

但我们已经快到了!我们了解到,我们可以使用任何 Grunt 实例方法在使用 grunt.loadTasks 导入的文件中。它们在 gruntthis 实例上可用。

在许多其他方法中,有一个宝贵的 grunt.config 方法。它允许我们在现有的 Grunt 配置中设置一个值。主要的配置是我们在 Gruntfile 中初始化的… 还记得吗?

重要的是我们可以定义任务配置的方式。这正是我们需要的!

// tasks/test.js

module.exports = function (grunt) {
  grunt.config('csso', {
    build: {
      files: { 'style.css': 'styles.css' }
    }
  })

  // same as
  // this.config('csso', {
  //   build: {
  //     files: { 'style.css': 'styles.css' }
  //   }
  // })
}

现在,让我们更新 Gruntfile 以记录当前配置。毕竟,我们需要看看我们做了什么

module.exports = function (grunt) {
  require('jit-grunt')(grunt)

  grunt.initConfig({
    testingValue: 123
  })

  grunt.loadTasks('tasks')

  // Log our current config
  console.log(grunt.config())

  grunt.registerTask('default', ['cssmin'])
}

运行 Grunt

$ grunt

… 这就是我们看到的内容

> {
    testingValue: 123,
    csso: {
      build: {
        files: {
          'style.css': 'styles.css'
        }
      }
    }
  }

grunt.config 在导入时设置 csso 值,因此 CSSO 任务现在已配置好,并在调用 Grunt 时准备运行。完美。

请注意,如果您以前使用的是 load-grunt-configs,您会使用这样的代码,其中每个文件都导出一个配置对象

// tasks/grunt-csso.js

module.exports = {
  target: {
    files: { 'style.css': 'styles.css' }
  }
}

这需要更改为一个函数,如上所述

// tasks/grunt-csso.js

module.exports = function (grunt) {
  grunt.config('csso', {
    build: {
      files: { 'style.css': 'styles.css' }
    }
  })
}

好吧,再最后一件事情… 这次是真真的!

将外部配置文件提升到下一个级别

我们学到了很多。加载任务、加载外部配置文件、使用 Grunt 方法定义配置… 很好,但有什么好处呢?

等等!

到目前为止,我们已经将所有任务配置文件都外部化了。因此,我们的项目目录看起来像这样

tasks
  ├─ grunt-browser-sync.js  
  ├─ grunt-cache-bust.js
  ├─ grunt-contrib-clean.js 
  ├─ grunt-contrib-copy.js  
  ├─ grunt-contrib-htmlmin.js   
  ├─ grunt-contrib-uglify.js
  ├─ grunt-contrib-watch.js 
  ├─ grunt-csso.js  
  ├─ grunt-nunjucks-2-html.js   
  ├─ grunt-postcss.js   
  ├─ grunt-processhtml.js
  ├─ grunt-responsive-image.js  
  ├─ grunt-sass.js  
  ├─ grunt-shell.js 
  ├─ grunt-sitemap-xml.js   
  ├─ grunt-size-report.js   
  ├─ grunt-spritesmith-map.mustache 
  ├─ grunt-spritesmith.js   
  ├─ grunt-standard.js  
  ├─ grunt-stylelint.js 
  ├─ grunt-tinypng.js   
  ├─ grunt-uncss.js 
  └─ grunt-webfont.js
gruntfile.js

这使得 Gruntfile 相对较小,事情似乎井井有条。但是,您只是看一眼这个冰冷无生命的任务列表,就能清楚地了解这个项目吗?它们究竟做了什么?流程是什么?

您能说出来 Sass 文件会经过 grunt-sass、然后 grunt-postcss:autoprefixer、然后 grunt-uncss,最后经过 grunt-csso 吗?是否很明显 clean 任务正在清理 CSS,或者 grunt-spritesmith 正在生成一个 Sass 文件,该文件也应该被选中,因为 grunt-watch 正在监视更改?

事情似乎乱七八糟。我们可能在外部化方面走得太远了!

所以,最后… 现在如果我告诉您,有一种更好的方法是根据功能对配置进行分组… 如何?与其使用一个不太有用的任务列表,我们将会得到一个明智的功能列表。怎么样?

tasks
  ├─ data.js 
  ├─ fonts.js 
  ├─ icons.js 
  ├─ images.js 
  ├─ misc.js 
  ├─ scripts.js 
  ├─ sprites.js 
  ├─ styles.js 
  └─ templates.js
gruntfile.js

这向我讲述了一个故事!但是我们该如何做到呢?

我们已经学习了 grunt.config。相信与否,您可以在一个外部文件中多次使用它来同时配置多个任务!让我们看看它是如何工作的

// tasks/styles.js

module.exports = function (grunt) {
  // Configuring Sass task
  grunt.config('sass', {
    build: {/* options */}
  })
  
  // Configuring PostCSS task
  grunt.config('postcss', {
    autoprefix: {/* options */}
  })
}

一个文件,多个配置。相当灵活!但我们忽略了一个问题。

我们应该如何处理诸如 grunt-contrib-watch 之类的任务?它的配置是一个整体的单体结构,包含了无法拆分的每个任务的定义。

// tasks/grunt-contrib-watch.js

module.exports = function (grunt) {
  grunt.config('watch', {
    sprites: {/* options */},
    styles: {/* options */},
    templates: {/* options */}
  })
}

我们不能简单地使用 grunt.config 来在每个文件中设置 watch 配置,因为它会覆盖已导入文件中的相同 watch 配置。将它保留在一个独立的文件中听起来也不像一个好选择——毕竟,我们希望将所有相关的事情都放在一起。

别担心!grunt.config.merge 来拯救!

grunt.config 明确地设置并覆盖 Grunt 配置中的任何现有值,而 grunt.config.merge 会递归地将值与其他 Grunt 配置文件中的现有值合并,从而为我们提供一个单一的 Grunt 配置。一个简单但有效的方法,可以将相关的事情放在一起。

一个例子

// tasks/styles.js

module.exports = function (grunt) {
  grunt.config.merge({
    watch: {
      templates: {/* options */}
    }
  })
}
// tasks/templates.js

module.exports = function (grunt) {
  grunt.config.merge({
    watch: {
      styles: {/* options */}
    }
  })
}

这将产生一个单一的 Grunt 配置

{
  watch: {
    styles: {/* options */},
    templates: {/* options */}
  }
}

正是我们所需要的!让我们将它应用到实际问题中——我们的样式相关配置文件。替换掉我们的三个外部任务文件

tasks
  ├─ grunt-sass.js
  ├─ grunt-postcss.js   
  └─ grunt-contrib-watch.js

… 使用单个 tasks/styles.js 文件将它们全部整合

module.exports = function (grunt) {
  grunt.config('sass', {
    build: {
      files: [
        {
          expand: true,
          cwd: 'source/styles',
          src: '{,**/}*.scss',
          dest: 'build/assets/styles',
          ext: '.compiled.css'
        }
      ]
    }
  })

  grunt.config('postcss', {
    autoprefix: {
      files: [
        {
          expand: true,
          cwd: 'build/assets/styles',
          src: '{,**/}*.compiled.css',
          dest: 'build/assets/styles',
          ext: '.prefixed.css'
        }
      ]
    }
  })

  // Note that we need to use `grunt.config.merge` here!
  grunt.config.merge({
    watch: {
      styles: {
        files: ['source/styles/{,**/}*.scss'],
        tasks: ['sass', 'postcss:autoprefix']
      }
    }
  })
}

现在,只需看一眼 tasks/styles.js 就可以很容易地知道样式有三个相关的任务。我相信你可以想象将这个概念扩展到其他分组的任务,比如所有你可能想要对脚本、图像或其他任何东西做的事情。这让我们获得了合理的配置组织。相信我,查找东西会容易得多。

就是这样!这就是我们所学到的全部要点。

总结一下

Grunt 已经不再是它第一次出现在舞台上时的宠儿了。但到目前为止,它是一个简单可靠的工具,能够很好地完成它的工作。通过适当的处理,它甚至减少了用更新的工具替换它的理由。

让我们回顾一下我们可以做些什么来有效地组织我们的任务

  1. 使用 jit-grunt 而不是 load-grunt-tasks 来加载任务。它是一样的,但速度快得令人难以置信。
  2. 将特定的任务配置从 Gruntfile 移到外部配置文件中,以保持组织良好。
  3. 使用原生 grunt.task.loadTasks 来加载外部配置文件。它很简单,但功能强大,因为它公开了 Grunt 的所有功能。
  4. 最后,考虑一个更好的组织配置文件的方法!按功能或领域分组,而不是按任务本身分组。使用 grunt.config.merge 来拆分复杂的任务,比如 watch

当然,请查看 Grunt 文档。经过这么多年,它仍然值得一读。

如果你想看一个现实世界的例子,请查看 Kotsu,一个基于 Grunt 的入门套件和静态网站生成器。你会在里面找到更多技巧。

你是否有关于如何更好地组织 Grunt 配置的更好想法?请在评论中分享!