专注于WEB前端开发, 追求更好的用户体验, 更好的开发体验 (长沙前端QQ群:234746733)

使用nodejs压缩合并javascript和css文件

对于压缩工具前端攻城师最常见的就是雅虎的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:

  1. (function() {return;})(); 会变成:function(){}(), 删除了return;
  2. 0 = {}; 还是不会报错;
  3. 未使用的方法/变量移除时,会输出WARN信息;
  4. function c(){e();2} 会把2删除,并有WARN信息;
  5. 提示信息的行号部分有偏差(少了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、合并文件部分可以采用异步方式。

/ 分类: 开发,实践 / TrackBackhttp://www.fantxi.com/blog/archives/compress-js-css-files-with-nodejs/trackback标签: nodejs

已有 12 条评论 »

  1. bcpxqz bcpxqz

    诶,和我的思路一样。

    1. kairyou kairyou

      呵, 欢迎多交流~ 这样做比较适合团队diy使用, 发现淘宝已经把uj/clean-css/less整合做成工具了(Kissy Pie), 貌似不错.

  2. noonnightstorm noonnightstorm

    您好,我是一名学生,可能项目经验不多。我不太明白“自动生成map的同时,针对文件生成md5,下次压缩根据md5判断,如果文件内容变动,才压缩并重新生成map。”为什么要做这么复杂的工序呢?呵呵!我只是一名菜鸟,不要鄙视我,呵呵!

    1. kairyou kairyou

      @noonnightstorm
      你好, MD5对比的意义就是跳过没有改变内容的文件. 如果只修改了几个文件, 压缩全部文件花费的时间要大于只压缩这几个的时间的(以前压缩过的被缓存了可以直接使用), 这种方案基本是秒压.

  3. 全冠清 全冠清

    感谢楼主文章
    最后一部分uglifyjs已经可以支持合并了
    uglifyjs 1.js 2.js 3.js -o min.js

    1. kairyou kairyou

      @全冠清
      谢谢提醒, 抽时间测试下新版. uglifyjs不敢乱升级, 以前一次升级差点出了问题.

  4. minster minster

    楼主您好,我是一名大四的学生,我用uglifyjs在命令行执行是正常的,但是加到ant的apply任务中,就没有生成压缩的文件了,可能是我的build.xml写得有问题,但是这方面资料不多,请问楼主有没有类似的例子(在ant的apply任务中执行uglifyjs)?

    1. kairyou kairyou

      @minster
      已经在群里讨论过了哦, 放弃ant, 试试grunt.

  5. Peder Peder

    Hi Kayrio,

    Great tool, although I'm not sure I understand how to use the following elements in the template:

    # @Version : \$Id\$

    ${1:import os}
    $0

    Any chance you could add a bit about it to the README.md file?

    谢谢
    Cheers from Canada... Peder :)

    1. kairyou kairyou

      Hi Peder, Thanks for your feedback.
      "@version $Id$" is for ident "$Id$"(http://goo.gl/Al8vds, goo.gl/tyfpTa, goo.gl/zT5Vhj);
      "${1:..}" and "$0" are snippets for Sublime(http://goo.gl/T9oQay)

  6. landy landy

    你好,我看了之后有点看不太懂,不过我现在就是用grunt的,我想知道如何把多个js压缩成 **/1.js/2.js/3.js这样的形式呢。

    我也是菜鸟,多谢不吝赐教!

    1. kairyou kairyou

      你好, 你意思是合并后的文件名是/1.js/2.js吗, 这样引用src="/../1.js/2.js"? 这个不是合并的范畴了. 其实这些JS是独立的,服务端把他们合并输出了, 可以搜索下combo(有PHP/apache/nginx等可供选择).

添加新评论 »