对于压缩工具前端攻城师最常见的就是雅虎的Yui Compressor / Google的Closure Compiler了。
对比其他压缩工具相对在JS和CSS压缩领域比较成熟,压缩率也比较好.
一般会选择结合ANT实现压缩并合并,效果也不错,但是比较偏向个人,团队协作每个人都部署java/ant有些麻烦。
对于个人开发使用ANT的方案也是相对不错的选择。对于团队,最好的解决办法是在服务端压缩,大家只需要登录并执行一个统一的脚本。
下面分享下大致的测试结果,和改用nodejs压缩合并js/css的原因。
js部分采用UglifyJS
1. 压缩jquery 1.8,253 KB:使用UglifyJS(以下简称UJ): 90.5 KB;使用Closure Compiler(以下简称CC): 91.1 KB。
2. 如果在闭包(function($, window, undefined) {...})(jQuery, window); 内的 unused function/variables
CC会被删除没使用的;UJ 默认全部保留,加上 pro.ast_lift_variables(ast) 才会删除未使用的函数/变量 (对象/数组 不会被删除)。
如果直接暴漏在window下的,两个基本差不多。
3. function c(){2}, CC 会警告,UJ不会
4. function c(){e()2},都会抛出异常,显示行数,错误原因。CC提示列错误,UJ不会。
5. function c(){e();2},都保留了2,CC 警告,UglifyJS 无信息
6. CC 一次显示JS里的全部错误(有些是前面的错误引起的, 所以部分是误报),UJ每次只显示一个错误。
7. 0 = {}; CC会报错,而UJ不会。
8. if语句都可很好的优化。
9. CC喜欢把变量/函数(结构简单的)内的语句,直接插入到使用它们的地方,UJ维持原样。
如果函数内的内容较少, CC会把函数的内容直接插入到调用它的地方,比如:
function c(){xxxxxxx('12345678901234567');}
function c(){xxxxxx.yyyyy('12345678901234');}
function c(){xxxxxx.yyyyy.zzzz('12345678901');}
function c(){if (X){alert('1234')};alert('12');}
当其他函数里调用c()时,会把c()方法删除,然后把c()内的内容移动到这里
(当里面的字符串长度+1后,就会直接使用原函数c(),所以CC这里是根据字符长度)。
10. 如果是很长的字符串, var str ='很长......很长'; 在其他函数内用到str,CC会把str的值直接插入,而UJ不会。
11. 10000 都会转成 1e4。
12. alert(3*7) 都会转成 alert(21)。
13. function c() {}; function b(){return; c();}; CC/UJ(开启 --lift-vars) 都会删除 c() 的代码。
14. 这段代码:(function() {return;})(); CC会删除,UJ不会。
综上,CC是编译器,有高级优化选项,UJ目前只能算是压缩器。CC喜欢把函数外的字符串值/内容简单的函数内容,直接插入到使用它的地方,所以有时这样反而增加了压缩后的文件大小。
对于压缩后的大小,UJ压缩的一般比CC稍大,一般1KB左右。
压缩速度,CC想的事情比较多,而且需要java,所以压缩慢,UJ 速度飞快。
如果网站结构复杂,JS比较多的时候,UJ的速度优势就非常明显了。
所以团队成员无JS新手,使用UJ是个不错的选择。
另外, uglifyjs2 也在开发中,比UJ1的压缩效果好一些,https://github.com/mishoo/uglifyjs2,
对比uglifyjs1:
(function() {return;})(); 会变成:function(){}(), 删除了return;
0 = {}; 还是不会报错;
未使用的方法/变量移除时,会输出WARN信息;
function c(){e();2} 会把2删除,并有WARN信息;
提示信息的行号部分有偏差(少了1行);
因为还是beta版,UJ2的一些问题都算正常的,UJ2由UJ1改进而来,测试中也没发现重大bug,所以采用UJ2还算靠谱。
PS: 最近应用发现2个bug:
没用的内部变量, 没删除完整, 剩下了 "var;"
压缩文件末尾没加上";"号, 当2个文件合并后如果这样, 问题就大了:
var a={};a.x=function(){console.log(this)} //file1
(function(){...})() //file2 // 变成a.x()()了..
所以上面的2个bug, 需要自己写2个正则解决掉.
css部分采用clean-css
CSS压缩找到的有:
https://github.com/GoalSmashers/clean-css
https://github.com/ded/sqwish
https://github.com/fczuardi/node-css-compressor
对比后,选择了clean-css,压缩速度和效果都还不错,目前发现的问题:.a{}这样的无内容的规则,不会被清理。
改用nodejs压缩,1是源码是JS的,发现bug可以快速解决;2是nodejs的异步多线程IO特性,可以多线程压缩,压缩速度提升明显;3是统一了环境,不需要再依赖java。并且合并文件也非常简单。
压缩合并的大体思路:
build.js:
var argv = process.argv, arguments = argv.splice(2);
用来接收传递的参数,比如可以:sudo ./build.js css {project path}
var buildType = arguments[0], projectPath = arguments[1];
简单的项目,就可以去project path的assets目录遍历待压缩的文件,进行压缩。
高级点的,可以把文件列表写到配置,var maps = require('map').maps; 然后遍历maps进行压缩合并,只压缩map的结构一维就够了,如果想压缩并合并,可以改成二维的结构。
再高级点,遍历文件夹得到待压缩的文件(想办法去掉不需要压缩的文件),再根据规则产生待合并的文件名,然后自动生成map。
再高级点,自动生成map的同时,针对文件生成md5,下次压缩根据md5判断,如果文件内容变动,才压缩并重新生成map。
当然,也不是后面的方法最好,选择适合自己的就是最好的。
大体上,这样就可以制作“傻瓜版”压缩工具了,只需要输入参数,其他的不需要管。
我们的做法是读取header / footer 的部分, 匹配标记生成待压缩的文件列表和合并后的目标文件名,比如:
<!-- target="pkg1.min.js" {{{ -->
<script src="a1.js"></script>...<script src="b1.js"></script>
<!-- }}} -->
<!-- target="pkg2.min.js" {{{ --> ... <!-- }}} -->
然后生成带MD5的map,对比文件是否改动,选择性压缩,再合并到target指向的目标文件。
开发时使用未压缩的,上线前压缩合并,再自动把header/footer未压缩的注释掉,加上合并后的JS/CSS。
使用UglifyJS2、clean-css的压缩代码,已经放到github,https://github.com/kairyou/f2e-tools/tree/master/libs。
require build-css-cc.js或build-js-uj2.js,就可以使用里面的build方法压缩了。
另外,压缩时需要发送错误时终止并提示,所以压缩时的读取是sync方式,但是生成文件map、产生MD5、合并文件部分可以采用异步方式。