對於壓縮公用程式前端攻城師最常見的就是雅虎的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、合并檔案部分可以採用非同步方式。