我的服务端JS文件合并工具
静态资源合并,老生常谈的话题了,目的就是减少http请求数,至于为什么要减少http请求大家应该都有所了解,这里就不多赘述,近期由于项目需要,自行开发了一个文件合并工具,由于源码在公司研发网络拿不出来,见谅!这里介绍一下工具的开发过程和大概实现思路,第一次发文到首页,请兄弟们多多指点。
最终效果,先睹为快^_^
最终合并后的效果
项目编译时自动合并所有js文件
经过测试:解析488个jsp文件,最终将314个js文件合并为77个,用时在一秒以内,因此打包时不会耗费太长时间。
对现有几个开源合并工具的分析
起初,本着不重复造轮子的想法,我也曾对现有的一些比较成熟的工具进行过分析:
1、minify:相关介绍
Minify 是用PHP5开发的应用,它会合并多个CSS或者JavaScript文件,移除一些不必要的空格和注释,进行gzip压缩,并且会设置浏览器的缓存头。
合并、压缩、缓存都有了,功能上基本已经能够满足现有要求,但是比较麻烦的是它是用php开发的,我们项目使用的是java,因此还需专门为其搭建一个php运行环境,再分配一个子域名用于内部请求。
2、nginx_concat_module:相关介绍
淘宝的开源nginx模块,在url中加入需要合并的文件路径以逗号隔开,使用两个问号“??”来触发文件的合并功能。
优点:只需要在nginx安装相关模块即可,不用对运行环境大动干戈,配置和使用方式简单。
缺点:没有使用nginx的项目就只能干瞪眼了,虽然我们项目有使用nginx,但是服务器属于运维组维护,要折腾服务器还得通过他们,麻烦;多个文件路径放到一个url当中,会导致url特别长。
以上两个工具还有一个共同点就是,在用户第一次访问的时候才做第一次合并,然后才会进行缓存处理,这样所有页面的第一个访问者会有一定的等待时间,并且如果第一次访问是并发访问,可能还要加锁去保证相同的文件只合并一次,这样又会导致更多用户等待第一次合并。
鉴于以上种种原因,我决定自行开发一个基于java环境、预合并与在线合并同步、可压缩、可缓存、最终访问的url较为简短的合并工具。
设计过程中的一些思考
1)是否所有的文件都需要合并
对于这个问题我是这么考虑的
首先,对于大多数页面都使用并且不易发生变化的公共资源不应当加入合并,比如jquery或者一些全站公共的js等,这些文件访问率很高,与页面上的其它资源合并后反而会导致内容的重复下载,不如让它独立并使用缓存,减少传输量(这里我之前看到网上看到许多说法跟我的更好相反,有些不解,有知道的童鞋能否给本人解惑一下)。
其次,对于某些本身文件就很大的资源也不应该加入合并,因为与这些文件合并后的最终文件会很大,传输时间会很长,很可能反而不如多几个请求来的更快一些,并且在合并序列中比较大的文件靠后的话还会导致前面一些较小文件代码延迟执行;经过一定量的测试后发现js文件尽量保持在100K以内比较合适,至于具体的文件判断没有用程序去智能判断,这个需要在开发过程中人工去控制了。
基于以上考虑,在合并工具中加入了黑名单,在合并过程中遇到黑名单中的时候将其过滤,不进行合并。但是需要注意的一点就是在过滤黑名单时要考虑到文件的依赖顺序,如果过滤的文件在合并序列中间的话就需要将合并被切为三段,合并后也是三个文件;在头或尾的话就是两个了,使用者可以自行配置,黑名单列表一个需要权衡的问题,还是交给使用者决定吧。
2)关于压缩
起初我把压缩功能加到了合并过程中去,但是后来发现这样不利于压缩更细粒度的控制,因此就把它取消了,压缩功能可以使用YUI的maven插件,在执行完合并以后调用压缩功能,这样在压缩的时候可以更精确的控制哪些文件要压缩(一些本身不会发生变化并且较大的文件可以人工压缩好,这样可以节省打包时的压缩时间)、是否混淆等等
3)关于缓存与版本控制
合并后的文件名是根据被合并的文件路径通过md5生成的字符串, 如果参与合并的文件路径发生变化,那么合并后的文件名也会随之变化。
对于文件内容变化的识别,之前设计时是考虑是在文件名中加入版本号以及文件的MD5进行处理,这样内容被修改后再合并的文件名就会发生变化,客户端会重新加载,后来考虑项目中使用了E-tag,内容变化了以后E-tag可以识别到,无需去做版本控制。如果不使用etag的话就要考虑一下缓存的问题了。
合并的工作过程
- jsp文件中使用自定义标签配置好需要合并的资源列表(如<focus:static type="text/javascript" src="/script/a.js,/script/b.js,/script/c.js"></focus:static>)
- 项目打包时调用maven插件titan-file-combiner-maven,根据相关配置解析目标路径下的jsp文件,获取所有需要合并的资源相对路径,文件解析工具返回的是一个字符串List,每一条记录中包含需要合并的多个资源的路径,以逗号隔开。
- 遍历上一步返回的List,调用合并工具titan-file-combiner进行合并操作,并将合并后的文件输出到对应的目标路径下,合并后的文件名根据所合并的文件路径经过一定的处理后生成。
- 在合并完成后还可以调用yahoo的YUIcompressor插件进行压缩并输出到项目输出路径。
- 在用户访问相关页面(即某个jsp)时,自定义标签<focus:static>再次调用titan-file-combiner(若之前的解析有遗漏,这里会再次进行合并操作)根据src的内容生成合并后的url(这里的url和第2步中的url是一致的,指向的是合并后的文件路径),然后生成最终的<script>标签最终形成静态的html传送到客户端。
- 页面在通过最终的url来访问合并后的资源。
合并所依赖的资源介绍
1)jericho-html-3.2.jar:html文件解析器(第三方jar包:用于编译期解析jsp文件,获取某个jsp页面中需要预合并的资源路径)
2)titan-file-combiner-1.0.jar:资源合并工具(自主开发:用于遍历项目路径下所有的jsp文件,并调用文件解析器进行解析,获取所有要合并的资源路径列表,并提供合并功能)
3)titan-file-combiner-maven-1.0.jar:maven插件(自主开发:用于在项目编译打包时调用,解析并预合并相关资源)
4)自定义标签<focus:static>:自定义标签(自主开发:用于渲染html时将开发者配置的资源路径替换为合并后的资源路径,并生成相应的html标签)
部分细节实现
1)对jericho-html-3.2.jar的使用
jericho-html-3.2.jar不仅可以解析文件中的html标签,同时还支持对部分服务端语言,如jsp,php等语言的解析,甚至还支持对某个url最终返回的页面进行解析,以获取到你想要的元素值(这里我用来解析jsp中自定义标签的src属性,以获取在访问这个页面时哪些资源需要进行合并)。
2)文件合并的实现
文件合并其实很简单,就是根据传入的参数去项目路径下遍历所有需要合并的文件通过字节流进行读取到内存,然后根据指定编码将字节数组转为字符串,然后进行拼接(拼接时注意在结尾添加"\n"否则可会导致部分代码被注释掉),最后将最终的字符串再写到目标路径即可。
3)合并后文件名的生成
这里我采用的是根据合并前传入的参数(即所有参与合并的资源相对路径)使用CRC32循环冗余校验生成最终的校验值,然后定义一个不含有重复字符的字符串,使用校验值循环除以字符串的长度取余,使用余数在去字符串中定位获取对应的字符,最终生成一个字符串。这样参与合并的资源不同或顺序不同最终生成的文件名也会不同,最终生成的文件名为4-6个字符的字符串。
4)使用时的参数配置
由于从打包发布到用户访问可能会有两个地方进行合并,并且也有两个地方需要生成合并后的文件名,因此两个地方的配置需要一致,因此这里的maven插件和自定义标签的配置我都实现了两套,一套是通过properties配置文件进行读取,两处读同一份配置文件就可以很方便的保证一致性;另一套就是在调用时自行传入参数配置,这样在测试时修改各个参数更方便一些。
5)开发过程的调试问题
开发过程中由于不需要使用插件进行打包,因此只有自定义标签会实时进行合并,因此开发时可以在自定义标签中配置不进行合并,就可以这样就可以方便的打断点调试了。
css文件的合并原理与js基本一致,由于项目中css已经使用另一套机制去合并,因此这里就没有再去实现。
我的合并工具介绍至此,请大家多多指点。