我们生活在 webpack 和 npm 脚本 的时代。无论好坏,它们都成为了打包和任务运行的主力,以及一些 Rollup、JSPM 和 Gulp 的片段。但让我们面对现实。您的一些旧项目仍在使用老式的 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 将自动加载指定目录中的所有 js
或 coffee
配置文件。干净利落!但是,如果您尝试使用它,您会发现它什么也没做。这是怎么回事呢?我们还需要做一件事情。
让我们再次查看 gruntfile.js
代码,这次不带注释
module.exports = function (grunt) {
require('jit-grunt')(grunt)
grunt.initConfig({})
grunt.loadTasks('tasks')
grunt.registerTask('default', ['cssmin'])
}
请注意,grunt.loadTasks
从 tasks
目录加载文件,但从未将其分配给我们的实际 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.loadTasks
将 this
和 grunt
传递给我们的当前 Grunt 实例——而不是它的副本。通过调用该引用上的方法,我们能够读取和修改我们主要的 Grunt 配置文件。
现在,我们需要实际配置一些内容!最后一件事情…
这次,让我们让配置加载真正生效
好吧,我们已经走了很长一段路。我们的任务是自动加载的,而且速度更快。我们学习了如何使用原生的 Grunt 方法加载外部配置。但是我们的任务配置还没有到位,因为它们最终并没有出现在 Grunt 配置中。
但我们已经快到了!我们了解到,我们可以使用任何 Grunt 实例方法在使用 grunt.loadTasks
导入的文件中。它们在 grunt
和 this
实例上可用。
在许多其他方法中,有一个宝贵的 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 已经不再是它第一次出现在舞台上时的宠儿了。但到目前为止,它是一个简单可靠的工具,能够很好地完成它的工作。通过适当的处理,它甚至减少了用更新的工具替换它的理由。
让我们回顾一下我们可以做些什么来有效地组织我们的任务
- 使用
jit-grunt
而不是load-grunt-tasks
来加载任务。它是一样的,但速度快得令人难以置信。 - 将特定的任务配置从 Gruntfile 移到外部配置文件中,以保持组织良好。
- 使用原生
grunt.task.loadTasks
来加载外部配置文件。它很简单,但功能强大,因为它公开了 Grunt 的所有功能。 - 最后,考虑一个更好的组织配置文件的方法!按功能或领域分组,而不是按任务本身分组。使用
grunt.config.merge
来拆分复杂的任务,比如watch
。
当然,请查看 Grunt 文档。经过这么多年,它仍然值得一读。
如果你想看一个现实世界的例子,请查看 Kotsu,一个基于 Grunt 的入门套件和静态网站生成器。你会在里面找到更多技巧。
你是否有关于如何更好地组织 Grunt 配置的更好想法?请在评论中分享!
我很高兴看到这篇文章。老实说,我有几个正在使用 Grunt 的活跃项目,我仍然喜欢它…
感谢你发表文章,兄弟,它帮了我的忙。
你如何获取每个任务的执行时间?
通过使用 time-grunt