玩转Grunt(一): Minification

1. Preface

在上一期Grunt初探一文中提到,Grunt将Javascript项目的构建任务(合并压缩、单元测试、测试覆盖率、静态代码检查等)自动化,大大优化了前端开发的工作流。今天让我们来看一看Grunt是如何帮助我们在持续部署的环境下解决Minification的相关问题。

2. Minification

在开始Grunt集成之前,我们来看一下什么是Minification。Minification简单的说就是为了减小前端js/css文件大小,通过去掉多余的注释和换行缩进等,使得下载和运行更快,提高用户体验。

2.1. When minify under continous deployment

那么Minification需要解决哪些问题呢?

2.1.1 It should create a minified file

众所周知,为解决前端Javascript、css加载的性能问题,前端代码在部署到产品环境时通常会进行Minify,也即合并压缩,将多个文件合并成一个文件,并对代码进行优化处理。那么我们所要解决的第一个问题是, Grunt需要能够对指定的一组Javascript或css文件(下面将以javascript文件为例)进行压缩处理。

index.html
1
2
3
4
5
6
// to be minified javascript files => app.min.js
<script src="js/app.js"></script>
<script src="js/services.js"></script>
<script src="js/controllers.js"></script>
<script src="js/filters.js"></script>
<script src="js/directives.js"></script>

21.2 It should replace the html fragement with minified file

第二,在前端项目中通常javascript文件被包含在一段html模版页面中,压缩完成后需要将压缩完成的文件替换,在没有Grunt之前往往需要人工替换或者写各式各样的脚本来进行文件操作,没有一致的处理脚本。那么我们要解决的第二个问题是,Grunt需要能够将被压缩的javascript目标文件用统一的API替换为压缩后的文件。

explore metadata
1
2
3
4
5
6
7
8
<html lang="en">

<body>
….
// replace all javascript files by app.min.js
<script src="js/app.min.js"></script>
</body>
</html>

2.1.3 It should not impact the development env

也就是说开发环境中保持源代码与压缩后的代码同时存在,互不影响,开发人员可以持续在开发环境中开发调试部署。

2.1.4 It should be fast

2.1.5 It should be able to debug

2.2 Minification tools

提到Minification不得不说一下各种高端大气工具,包括Google的Closure CompilerUglifyYUI Compressor。在第一部分已经提到了Minification的目的就是减小文件大小,那么要实现一个Minification工具需要包含代码解析器,它需要理解代码语法;需要包含压缩规则,它需要对代码进行规则匹配;需要包含规则处理器,在代码满足一定规则时进行代码替换,并输出压缩结果。Minification工具的不同即体现在以上解析器、规则匹配以及处理算法。

那么我们如何选取一个适合项目的Minification工具呢? 结合我们所要达到的目标,压缩的正确性、压缩速度、压缩大小以及工具的易用性、可扩展性都在考量之内,其中前三条是重中之重。

2.2.1 压缩正确性

Closure Compiler提供了两种压缩模式,simple和advance。这两种模式中都有一些对代码的限制,在simple方式下不能够处理with与eval;而advance,代码必须严格按照某些convention才能获取压缩上的性能优势,然而即使如此也会导致压缩后的代码出现破坏。

Uglify2同样有一个可与Closure Compiler媲美的解析器,但没有对被压缩代码的严格限制,能够理解eval和with关键词。它能够产生以object组织的AST,其它的压缩操作均基于此AST,包括source map, scope analyzer, transform等,并且它有大量的测试代码保证了压缩结果的正确性。

2.2.2 压缩速度

基于Uglify提供的数据以及项目代码上的测试,Uglify的压缩速度要远远快于Closure Compiler,基本上是数量级提升。在需要快速反馈持续部署的环境下,不得不承认这个优点很吸引人。

2.2.3 压缩大小

Uglify2在压缩字节上并不占优势,然而在Gzip之后是具有可比性的。

2.2.4 易用性

Closure Compiler和YUI Compressor都是基于Java的,而Uglify是基于JavaScript,在nodejs平台开发,使用npm进行包管理,这样前端项目的依赖管理开发平台趋向一致,降低维护成本。

综上分析,我们选用了Uglify v2.

3. Make it by Powerful Grunt plugins

在选定了压缩工具后,我们今天的主角, Grunt Plugins,UglifyuseMinCleanCopyConcat就开始一一出场了。

3.1 Project Requirement

给定我们的项目文件index.html,包含一组javascript文件,如下

index.html
1
2
3
4
5
6
7
8
9
10
11
12
<html lang="en">

<body>
….
<script src="js/app.js"></script>
<script src="js/services.js"></script>
<script src="js/controllers.js"></script>
<script src="js/filters.js"></script>
<script src="js/directives.js"></script>

</body>
</html>

需要将其中javascript自动压缩为一个文件,并替换。

3.2 Step1: Concat files

使用插件Concat,目的将所有javascript文件合并成一个文件,具体配置参数参见文档

Gruntfile.js
1
2
3
4
5
6
7
8
9
10
11
12
13
module.exports = function(grunt) {
'use strict';

grunt.initConfig({
concat:
{ 'app/dest/app.all.js':
[ 'app/dest/js/*.js']
}

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

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

3.3 Step2: Minify by Uglify2

使用插件Uglify2,目的将第一步产生的文件压缩,具体配置参见文档,这里只采用了uglify的缺省配置。

Gruntfile.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

module.exports = function(grunt) {
'use strict';

grunt.initConfig({
concat:
{
'app/dest/app.all.js': [ 'app/dest/js/*.js']
},
uglify:
{
'app/dest/app.min.js': 'app/dest/app.all.js',
options: {
report: 'min'
}
}
grunt.loadNpmTasks('grunt-contrib-uglify');
grunt.loadNpmTasks('grunt-contrib-concat');
grunt.registerTask('default', ['concat', 'uglify']);
});

3.4 Step3: Replace references to minified file

使用插件usemin,目的将index页面中的javascript块用第二步产生的mini文件替换,具体配置参数参见文档

在开始之前我们先来看看usemin插件,usemin是一个多任务插件,它包括两个任务,useminPrepare和usemin。

useminPrepare提供了一个API,用来检测html页面中的脚本块,包括脚本文件的源路径,目的路径,从而更新后续需要使用到的Grunt任务的配置信息,i.e. concat,uglify,cssmin以及requirejs。useminPrepare只是分析文件,获取文件及路径信息,不更新内容。而usemin则进行文件引用替换,将源文件中的文件块替换为压缩文件。

3.4.1 useminPrepare

Gruntfile.js
1
2
3
4
5
6
7
8
9
10
11
12
13
module.exports = function(grunt) {
'use strict';

grunt.initConfig({
useminPrepare : {
html: ['<%= pkgInfo.sourceDir %>/index*.html'], //search target files
options: {
dest: '<%= pkgInfo.packageDir %>' //output of concat/uglify,etc.
}
}
grunt.loadNpmTasks('grunt-usemin');
grunt.registerTask('default', ['useminPrepare']);
});

在命令行运行grunt,得到如下结果。

image

可以看到useminPrepare已经帮助我们自动配置了concat,uglify针对的源文件以及目的文件的路径信息,相应的step1和step2中的配置就可以去掉了。一个修改后的完整版Gruntfile.js如下:

Gruntfile.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
module.exports = function(grunt) {
'use strict';

grunt.initConfig({
useminPrepare : {
html: ['<%= pkgInfo.sourceDir %>/index*.html'], //search target files
options: {
dest: '<%= pkgInfo.packageDir %>' //output of concat/uglify,etc.
}
},
uglify: {
options: {
report: 'min'
}
}

grunt.loadNpmTasks('grunt-contrib-concat');
grunt.loadNpmTasks('grunt-contrib-uglify');
grunt.loadNpmTasks('grunt-usemin');
grunt.registerTask('default', ['useminPrepare', 'concat', 'uglify']);
});

3.4.2 usemin

压缩文件准备完成后,我们就进行最重要的文件引用替换部分。

Gruntfile.js
1
2
3
4
5
6
7
8
 usemin:{
html: ['<%= pkgInfo.packageDir %>/index*.html'],
options: {
dirs: ['<%= pkgInfo.packageDir %>']
}
}
grunt.registerTask('default', ['useminPrepare', 'concat', 'uglify', 'usemin']);

运行结果看看。

image

Yeah! 大功告成!index.html页面中的javascript代码块被成功替换。

最终的Grunt文件参见我的Github项目Grunt-Integration,文件中多了clean与copy两个任务,那是干什么的呢,就留给诸位了。

3.5 不得不说的angularJS Minification

在对angularJS项目进行minify的时候需要注意,angularJS 定义对象时需要使用string字符串方式注入,也就是

Gruntfile.js
1
2
3
4

angular.module('myModuleName').controller('MyCtrl', ['$scope',function ($scope) { //注意$scope
// ...
}]);

而不是

Gruntfile.js
1
2
3
angular.module('myModuleName').controller('MyCtrl', function ($scope) { //注意$scope
// ...
});

ngmin插件可以对其进行提前处理,将第二种方式转换为第一种,但我个人并不推荐使用ngmin。理由很简单,我认为团队需要在如何使用API问题上达成一致,由团队成员讨论不同方式的优点与缺点,并最终选择一种API使用方式,而不是多种并存。利用ngmin预处理是隐藏了问题,而不是解决问题。

4. Summary

通过上述实践可以看出Grunt的巨大优势,它帮助我们用统一的接口配置需要完成的各种任务,将各种前台工具无缝嵌入到构建脚本中,能够快速实现持续集成、持续部署,同时降低维护成本。你也来试试吧。

参考

Share Comments