使用 Grunt 作为前端构建工具有一段时间了,很感谢 Grunt 开发团队和众多的插件贡献者,得益于 Grunt 及众多的插件,我的前端开发工作效率提升了很多。
痛点
不过,项目目录下的 Gruntfile.js 文件却是越来越大。有的时候为某个功能的开发配置了一个简单的文件合并的任务,可是由于 Grunt 执行时要加载整个 Gruntfile.js 文件,执行内部配置的所有的插件加载、任务注册的流程,结果花费了不少额外的时间。相信随着工作的进一步开展,Gruntfile.js 里面引入的插件也会进一步增加,配置的任务也会越来越多。且不说执行任务的时间,仅仅是查看并修改某个任务的配置也会复杂起来。
可以先看看下面“功能实现”小节的例子中的目录结构和 Gruntfile 文件内容,感受下我的痛苦。
需求
如果可以把 Gruntfile.js 拆分一下就好了。
需求分析
拆分成什么样呢?
由于这个问题还要自己解决,分析的过程其实也是我考虑如何实现的过程。
一开始想,Gruntfile.js 其实是在 Node 中执行的代码,所以可以在其内部搞点小手段,根据执行 grunt 命令时额外传入的参数来加载不同的 外部任务配置文件,外部任务配置文件中是特定模块或功能相关的构建任务。这样以来,单个任务配置文件中只注册需要的插件、任务,文件也会比较简单,运行起来也就比较快了。
能否实现?
那这个通过 Gruntfile.js 再进一步加载特定的外部任务配置文件的流程该如何实现呢?
再看看 grunt 的源码吧,看下能否从中找些灵感。
一看不要紧,原来 grunt 项目中也有这种类似的需求,不过实现方式是这样的:
源码地址:
https://github.com/gruntjs/grunt/blob/master/internal-tasks/subgrunt.js#L19
看了这个我想了想,通过一个 Gruntfile.js 作为入口,再进一步加载其他的配置文件,弱爆了。不需要这么麻烦,直接写个脚本来根据传入参数调用不同的 Gruntfile 不就是了!
拆分出的任务配置文件的路径定位问题
虽然可以把任务写到拆分的配置文件中,可是还是想将项目的根目录作为配置文件中路径定位的基准。不然,一方面现有的任务配置如果拆分到某个子目录下的配置文件中,任务配置中的文件路径由于是相对路径,还得随之修改,另一方面,新建任务的时候,也要考虑路径问题,并且执行任务命令所在的路径跟任务配置文件中的路径基准不一样,好麻烦....(是的,我描述得就很麻烦)。
然而,grunt 在运行时,不仅可以指定 Gruntfile 的位置(--gruntfile
参数),还可以指定路径的基准目录(--base
参数)。
所以,尽管将任务配置文件拆分到了子目录中,甚至为了组织方便,有几层目录,但执行任务时,仍然可以将项目根目录作为解析相对路径的基准位置。
功能实现
啰嗦那么多,对于程序员而言,倒不如直接上代码来得简洁明了。所以,我还是画个图吧....
原有项目目录:
- src/
- moduleA/
- main.js
- main.less
- moduleB/
- dist/
- moduleA/
- main.bundle.js
- main.css
- moduleB/
- node_modules
- Gruntfile.js
- package.json
原有 Gruntfile.js:
module.exports = function (grunt) {
grunt.initConfig({
browserify: {
mod_a: {
src: 'src/moduleA/main.js',
dest: 'dist/moduleA/main.bundle.js'
}
},
uglify: {
mod_b: {
src: 'src/moduleB/main.js',
dest: 'dist/moduleB/main.min.js'
}
},
less: {
mod_a: {
src: 'src/moduleA/main.less',
dest: 'dist/moduleA/main.css'
},
mod_b: {
src: 'src/moduleB/main.less',
dest: 'dist/moduleB/main.css'
}
}
})
grunt.loadNpmTasks('grunt-contrib-uglify')
grunt.loadNpmTasks('grunt-contrib-less')
grunt.loadNpmTasks('grunt-browserify')
grunt.registerTask('mod_a', ['browserify:mod_a', 'less:mod_a'])
grunt.registerTask('mod_b', ['uglify:mod_a', 'less:mod_b'])
}
当然,上面只是一个示例,假设有两个模块需要进行构建,不过已经可以看到配置文件都这么长了,而且两个模块尽管不相关,彼此的任务配置也要写到一起。
成果:xsm-cli。
按照 xsm-cli 工具的设计(对,就是我设计的),可以把上面的配置文件按任务进行拆分,拆分后的目录结构:
- src/
- moduleA/
- main.js
- main.less
- moduleB/
- dist/
- moduleA/
- main.bundle.js
- main.css
- moduleB/
- node_modules
- *grunt
- *moduleA/Gruntfile.js
- *moduleB/Gruntfile.js
- *xsmfile.js
- package.json
注意,上面目录结构中与原有结构不同的地方我用 * 标记了一下。然后来分别看下相应的内容。
moduleA/Gruntfile.js:
module.exports = function (grunt) {
grunt.initConfig({
browserify: {
mod_a: {
src: 'src/moduleA/main.js',
dest: 'dist/moduleA/main.bundle.js'
}
},
less: {
mod_a: {
src: 'src/moduleA/main.less',
dest: 'dist/moduleA/main.css'
}
}
})
grunt.loadNpmTasks('grunt-contrib-less')
grunt.loadNpmTasks('grunt-browserify')
grunt.registerTask('default', ['browserify:mod_a', 'less:mod_a'])
}
moduleB/Gruntfile.js:
module.exports = function (grunt) {
grunt.initConfig({
uglify: {
mod_b: {
src: 'src/moduleB/main.js',
dest: 'dist/moduleB/main.min.js'
}
},
less: {
mod_b: {
src: 'src/moduleB/main.less',
dest: 'dist/moduleB/main.css'
}
}
})
grunt.loadNpmTasks('grunt-contrib-uglify')
grunt.loadNpmTasks('grunt-contrib-less')
grunt.registerTask('default', ['uglify:mod_a', 'less:mod_b'])
}
看上面两个配置文件,现在是不是清爽得多了,只包含模块相关的任务。
再来看下 xsmfiles.js 是如何配置的:
module.exports = {
'mod-a': 'moduleA/Gruntfile.js',
'mod-B': 'moduleB/Gruntfile.js'
}
也很简单吧,就是映射了一下拆分后的任务配置文件。
怎么用呢?
首先得安装下 xsm-cli 这个工具(grunt-cli 以及 grunt 和相关插件在拆分前就应该是已经安装了,不再赘述):
npm install xsm-cli -g
在项目目录下执行命令,如果想执行 mod-a 任务:
xsm mod-a
OK!
其实每个拆分出的任务配置文件,就是一个完整的 Gruntfile 配置文件,所以,也可以执行其中的单个任务,例如:
xsm mod-a less
这样就只执行了模块 A 的 less 编译的任务。
具体实现的代码也没有多少行,感兴趣请阅读源码吧:
https://github.com/xsm-ue/xsm-cli/blob/master/bin/xsm
小结
以上就是我解决工作中使用 Grunt 中任务配置的痛点的过程,写下来做个记录吧。