编写可维护的 Gruntfile.js

使用Grunt已经有很长一段时间了,不得不感叹其社区的壮大,各种插件层出不穷。而在这期间我也换过几种方式来组织Gruntfile.js,但都不是很理想,直到前段时间看到load-grunt-tasks这个插件以及More maintainable Gruntfiles这篇文章后,我就把项目中的Gruntfile.js都按照该文章作者所述的方式重新组织了一遍。

我就暂且把这种方式用自己的文字记录一下并分享给正在使用Grunt的同学们吧,不过本文也不算是对《More maintainable Gruntfiles》的翻译呐,毕竟我E文太差~

load-grunt-tasks 插件

首先介绍下load-grunt-tasks这个插件。

我们一般都会把所有用到的插件以及插件的配置写到Gruntfile.js里面,对于小项目来说这个文件最终或许不是很大,但是对于大项目、有很多配置或者很多自定义任务的项目来说,最后这个文件都会变得越来越长,维护起来就成了麻烦。比如下面这样:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
module.exports = function(grunt) {
grunt.initConfig({
pkg: grunt.file.readJSON('package.json'),
concat: {
options: {
separator: ';'
},
dist: {
src: ['src/\*\*/\*.js'],
dest: 'dist/<%= pkg.name %>.js'
}
},
uglify: {
options: {
banner: '/\*! <%= pkg.name %> <%= grunt.template.today("dd-mm-yyyy") %> \*/\n'
},
dist: {
files: {
'dist/<%= pkg.name %>.min.js': ['<%= concat.dist.dest %>']
}
}
},
qunit: {
files: ['test/\*\*/\*.html']
},
jshint: {
files: ['gruntfile.js', 'src/\*\*/\*.js', 'test/\*\*/\*.js'],
options: {
globals: {
jQuery: true,
console: true,
module: true,
document: true
}
}
},
watch: {
files: ['<%= jshint.files %>'],
tasks: ['jshint', 'qunit']
}
});

grunt.loadNpmTasks('grunt-contrib-uglify');
grunt.loadNpmTasks('grunt-contrib-jshint');
grunt.loadNpmTasks('grunt-contrib-qunit');
grunt.loadNpmTasks('grunt-contrib-watch');
grunt.loadNpmTasks('grunt-contrib-concat');

grunt.registerTask('test', ['jshint', 'qunit']);
grunt.registerTask('default', ['jshint', 'qunit', 'concat', 'uglify']);
};

这是一个很标准的Gruntfile.js,显然也算是很简短的了,但是看起来也有点累觉不爱。于是load-grunt-tasks出来帮我们解决了一部分问题。

它会自动读取并加载项目packge.json文件中devDependencies配置下以grunt-*开头的依赖库。于是乎我们就可以用一行代码来搞定上面代码中很多行的loadNpmTasks了。

1
2
3
4
5
6
7
require('load-grunt-tasks')(grunt);
// 就代替了以下全部
grunt.loadNpmTasks('grunt-contrib-uglify');
grunt.loadNpmTasks('grunt-contrib-jshint');
grunt.loadNpmTasks('grunt-contrib-qunit');
grunt.loadNpmTasks('grunt-contrib-watch');
grunt.loadNpmTasks('grunt-contrib-concat');

Gruntfile.js 继续廋身

load-grunt-tasks插件替Gruntfile.js省去了那些反复书写的方法调用,接下来就是将整个Gruntfile.js变得干净清爽的步骤了。那就是把上面的各种config分离出去,让它们各自代表自己是属于哪个插件,而不是一口气全写在一起。当然,还有各种用registerTask方法定义的自定义任务,也该单独放到相应的文件中。

自定义任务迁移

首先,在项目根目录下建一个名为tasks的目录,在这个目录下来编写各种自定义任务。可以一个任务一个 js 文件,也可以多个简单任务在一个 js 文件,看个人喜好吧。然后在Gruntfile.js中用一行代码来载入这些自定义任务:

1
grunt.loadTasks('tasks');  // 即刚刚新建目录的名称

配置项迁移

然后再在这个目录下新建一个名为options的子目录(tasks/options),来存放之前说的那些config们。为每一类config建一个 js 文件,并以配置项节点名作为文件名称,比如下面这样:

1
2
3
4
5
6
tasks
└── options
└── concat.js
└── uglify.js
└── qunit.js
└── jshint.js

然后在每个文件中导出对应的配置项,拿concat.js来说:

1
2
3
4
5
6
7
8
9
module.exports = exports = {
options: {
separator: ';'
},
dist: {
src: ['src/\*\*/\*.js'],
dest: 'dist/<%= pkg.name %>.js'
}
};

最后在Gruntfile.js里用require将配置逐个引入即可,也可以封装一个函数来做这件事情。

1
2
3
4
5
6
7
8
9
10
11
function loadConfig(configPath) {
var config = {};

glob.sync('\*', { cwd: configPath })
.forEach(function(configFile) {
var prop = configFile.replace(/\.js$/, '');
config[prop] = require(path.join(__dirname, configPath, configFile));
});

return config;
}

再改写Gruntfile.jsinitConfig的调用即可。

1
2
3
4
5
6
var _ = require('lodash');
var config = {
pkg: grunt.file.readJSON('package.json')
};
_.extend(config, loadConfig('./tasks/options/'));
grunt.initConfig(config);

写在最后

于是乎在每个项目中Gruntfile.js几乎一致,而且也几乎不会再变更。Gruntfile.js、自定义任务、任务配置项各司其职,需要变化时只需对相应文件做出调整即可。

就在前些天,又一位 GitHuber 将这个思路封装成了一个库:load-grunt-config,感兴趣的同学可以看看。

最终的Gruntfile.js可以查看这个例子:https://github.com/heroicyang/cnodeclub/blob/master/Gruntfile.js

参考资料

load-grunt-tasks: https://npmjs.org/package/load-grunt-tasks
More maintainable Gruntfiles: http://www.thomasboyt.com/2013/09/01/maintainable-grunt.html