最近在对一个web系统作性能优化.
而对用到的静态资源文件的压缩整合则是前端性能优化中很重要的一环.
好处不只在于可以减少请求的文件体积,并且可以减小浏览器的http请求数.
由于是基于java的web系统,而且使用的是nginx+tomcat作为服务器.
最后考虑用wro4j和maven plugin在编译期间压缩静态资源.
优化前:
基本上全部的jsp都引用了这一大坨静态文件:
Html代码
- <link rel="stylesheet" type="text/css" href="${ctxPath}/css/skin.css"/>
- <link rel="stylesheet" type="text/css" href="${ctxPath}/css/jquery-ui-1.8.23.custom.css"/>
- <link rel="stylesheet" type="text/css" href="${ctxPath}/css/validationEngine.jquery.css"/>
-
- <script type="text/javascript">var GV = {ctxPath: '${ctxPath}',imgPath: '${ctxPath}/css'};</script>
- <script type="text/javascript" src="${ctxPath}/js/jquery-1.7.2.min.js"></script>
- <script type="text/javascript" src="${ctxPath}/js/jquery-ui-1.8.23.custom.min.js"></script>
- <script type="text/javascript" src="${ctxPath}/js/jquery.validationEngine.js"></script>
- <script type="text/javascript" src="${ctxPath}/js/jquery.validationEngine-zh_CN.js"></script>
- <script type="text/javascript" src="${ctxPath}/js/jquery.fixedtableheader.min.js"></script>
- <script type="text/javascript" src="${ctxPath}/js/roll.js"></script>
- <script type="text/javascript" src="${ctxPath}/js/jquery.pagination.js"></script>
- <script type="text/javascript" src="${ctxPath}/js/jquery.rooFixed.js"></script>
- <script type="text/javascript" src="${ctxPath}/js/jquery.ui.datepicker-zh-CN.js"></script>
- <script type="text/javascript" src="${ctxPath}/js/json2.js"></script>
- <script type="text/javascript" src="${ctxPath}/js/common.js"></script>
引用的文件不少,而且文件体积没有压缩,致使页面请求的时间很是长.
另外还有一个问题,就是为了可以充分利用浏览器的缓存,静态资源的文件名称最好可以作到版本化控制.
这样前端web服务器就能够放心大胆的开启缓存功能而不用担忧缓存过时问题,由于若是一旦静态资源文件有修改的话,
会从新生成一个文件名称.
下面我根据本身项目的经验,来介绍下如何较好的解决这两个问题.
分两步进行.
第一步:引入wro4j,在编译时期将上述分散的多个文件整合成少数几个文件,而且将文件最小化.
第二步:在生成的静态资源文件的文件名称上加入时间信息
这是两步优化以后的引用状况:
Html代码
- ${platform:cssFile("/wro/basic") }
- <script type="text/javascript">var GV = {ctxPath: '${ctxPath}',imgPath: '${ctxPath}/css'};</script>
- ${platform:jsFile("/wro/basic") }
- ${platform:jsFile("/wro/custom") }
只引用了1个css文件,2个js文件.http请求从10几个减小到3个,而且总体文件体积缩小了近一半.
下面介绍优化流程.
第一步:合并而且最小化文件.
1.添加wro4j的maven依赖
Xml代码
- <wro4j.version>1.6.2</wro4j.version>
-
- ...
-
- <dependency>
- <groupId>ro.isdc.wro4j</groupId>
- <artifactId>wro4j-core</artifactId>
- <version>${wro4j.version}</version>
- <exclusions>
- <exclusion>
-
- <!-- 由于项目中的其余jar包已经引入了不一样版本的slf4j,因此这里避免jar重叠因此不引入 -->
- <groupId>org.slf4j</groupId>
- <artifactId>slf4j-api</artifactId>
- </exclusion>
- </exclusions>
- </dependency>
2.添加wro4j maven plugin
Xml代码
- <plugin>
- <groupId>ro.isdc.wro4j</groupId>
- <artifactId>wro4j-maven-plugin</artifactId>
- <version>${wro4j.version}</version>
- <executions>
- <execution>
- <phase>compile</phase>
- <goals>
- <goal>run</goal>
- </goals>
- </execution>
- </executions>
- <configuration>
- <targetGroups>basic,custom</targetGroups>
-
- <!-- 这个配置是告诉wro4j在打包静态资源的时候是否须要最小化文件,开发的时候能够设成false,方便调试 -->
- <minimize>true</minimize>
- <destinationFolder>${basedir}/src/main/webapp/wro/</destinationFolder>
- <contextFolder>${basedir}/src/main/webapp/</contextFolder>
-
- <!-- 这个配置是第二步优化须要用到的,暂时忽略 -->
- <wroManagerFactory>com.rootrip.platform.common.web.wro.CustomWroManagerFactory</wroManagerFactory>
- </configuration>
- </plugin>
若是开发环境是eclipse的话,能够下载m2e-wro4j这个插件.
下载地址:http://download.jboss.org/jbosstools/updates/m2e-wro4j/
这个插件的主要功能是可以帮助咱们在开发环境下修改对应的静态文件,或者pom.xml文件的时候可以自动生成打包好的js和css文件.
对开发来讲就会方便不少.只要修改源文件就能看见修改后的结果.
3.在WEB-INF目录下添加wro.xml文件,这个文件的做用就是告诉wro4j须要以怎样的策略打包jss和css文件.
Java代码
- <?xml version="1.0" encoding="UTF-8"?>
- <groups xmlns="http://www.isdc.ro/wro" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
- xsi:schemaLocation="http://www.isdc.ro/wro wro.xsd">
-
- <group name="basic">
- <css>/css/basic.css</css>
- <css>/css/skin.css</css>
- <css>/css/jquery-ui-1.8.23.custom.css</css>
- <css>/css/validationEngine.jquery.css</css>
-
- <js>/js/jquery-1.7.2.min.js</js>
- <js>/js/jquery-ui-1.8.23.custom.min.js</js>
- <js>/js/jquery.validationEngine.js</js>
- <js>/js/jquery.fixedtableheader.min.js</js>
- <js>/js/roll.js</js>
- <js>/js/jquery.pagination.js</js>
- <js>/js/jquery.rooFixed.js</js>
- <js>/js/jquery.ui.datepicker-zh-CN.js</js>
- <js>/js/json2.js</js>
- </group>
-
- <group name="custom">
- <js>/js/jquery.validationEngine-zh_CN.js</js>
- <js>/js/common.js</js>
- </group>
-
- </groups>
官方文档:http://code.google.com/p/wro4j/wiki/WroFileFormat
其实这个配置文件很好理解,若是不肯看官方文档的朋友我在这简单介绍下.
上面这样配置的目的就是告诉wro4j要将
<css>/css/basic.css</css>
<css>/css/skin.css</css>
<css>/css/jquery-ui-1.8.23.custom.css</css>
<css>/css/validationEngine.jquery.css</css>
这四个文件整合到一块儿,生成一个叫basic.css的文件到指定目录(wro4j-maven-plugin里配置的),将
<js>/js/jquery-1.7.2.min.js</js>
<js>/js/jquery-ui-1.8.23.custom.min.js</js>
<js>/js/jquery.validationEngine.js</js>
<js>/js/jquery.fixedtableheader.min.js</js>
<js>/js/roll.js</js>
<js>/js/jquery.pagination.js</js>
<js>/js/jquery.rooFixed.js</js>
<js>/js/jquery.ui.datepicker-zh-CN.js</js>
<js>/js/json2.js</js>
这几个文件整合到一块儿,生成一个叫basic.js的文件到指定目录.
最后将
<js>/js/jquery.validationEngine-zh_CN.js</js>
<js>/js/common.js</js>
这两个文件整合到一块儿,,生成一个叫custom.js的文件到指定目录.
第一步搞定,这时候若是你的开发环境是eclipse而且安装了插件的话,应该就能在你工程的%your webapp%/wor/目录下看见生成好的
basic.css,basic.js和custom.js这三个文件了.
而后你再将你的静态资源引用路径改为
Html代码
- <link rel="stylesheet" type="text/css" href="${ctxPath}/wro/basic.css"/>
- <script type="text/javascript" src="${ctxPath}/wro/basic.js"></script>
- <script type="text/javascript" src="${ctxPath}/wro/custom.js"></script>
就ok了.每次修改被引用到的css或js文件的时候,这些文件都将从新生成.
若是开发环境是eclipse可是没有安装m2e-wro4j插件的话,pom.xml可能须要额外配置.
请参考:
https://community.jboss.org/en/tools/blog/2012/01/17/css-and-js-minification-using-eclipse-maven-and-wro4j
第二步:给生成的文件名称中加入时间信息并经过el自定义函数引用脚本文件.
1. 建立DailyNamingStrategy类
Java代码
- public class DailyNamingStrategy extends TimestampNamingStrategy {
-
- protected final Logger log = LoggerFactory.getLogger(DailyNamingStrategy.class);
-
- @Override
- protected long getTimestamp() {
- String dateStr = DateUtil.formatDate(new Date(), "yyyyMMddHH");
- return Long.valueOf(dateStr);
- }
-
-
-
- }
2.建立CustomWroManagerFactory类
Java代码
- //这个类就是在wro4j-maven-plugin里配置的wroManagerFactory参数
- public class CustomWroManagerFactory extends
- DefaultStandaloneContextAwareManagerFactory {
- public CustomWroManagerFactory() {
- setNamingStrategy(new DailyNamingStrategy());
- }
- }
上面这两个类的做用是使用wro4j提供的文件命名策略,这样生成的文件名就会带上时间信息了.
例如:basic-2013020217.js
可是如今又会发现一个问题:若是静态资源文件名称不固定的话,那怎么样引用呢?
这时候就须要经过动态生成<script>与<link>来解决了.
由于项目使用的是jsp页面,因此经过el自定义函数来实现标签生成.
3.建立PlatformFunction类
Java代码
- public class PlatformFunction {
-
- private static Logger log = LoggerFactory.getLogger(PlatformFunction.class);
-
-
- private static ConcurrentMap<String, String> staticFileCache = new ConcurrentHashMap<>();
-
- private static AtomicBoolean initialized = new AtomicBoolean(false);
-
- private static final String WRO_Path = "/wro/";
-
- private static final String JS_SCRIPT = "<script type=\"text/javascript\" src=\"%s\"></script>";
- private static final String CSS_SCRIPT = "<link rel=\"stylesheet\" type=\"text/css\" href=\"%s\">";
-
- private static String contextPath = null;
-
- /**
- * 该方法根据给出的路径,生成js脚本加载标签
- * 例如传入参数/wro/custom,该方法会寻找webapp路径下/wro目录中以custom开头,以js后缀结尾的文件名称名称.
- * 而后拼成<script type="text/javascript" src="${ctxPath}/wro/custom-20130201.js"></script>返回
- * 若是查找到多个文件,返回根据文件名排序最大的文件
- * @param str
- * @return
- */
- public static String jsFile(String filePath) {
- String jsFile = staticFileCache.get(buildCacheKey(filePath, "js"));
- if(jsFile == null) {
- log.error("加载js文件失败,缓存中找不到对应的文件[{}]", filePath);
- }
- return String.format(JS_SCRIPT, jsFile);
- }
-
- /**
- * 该方法根据给出的路径,生成css脚本加载标签
- * 例如传入参数/wro/custom,该方法会寻找webapp路径下/wro目录中以custom开头,以css后缀结尾的文件名称名称.
- * 而后拼成<link rel="stylesheet" type="text/css" href="${ctxPath}/wro/basic-20130201.css">返回
- * 若是查找到多个文件,返回根据文件名排序最大的文件
- * @param str
- * @return
- */
- public static String cssFile(String filePath) {
- String cssFile = staticFileCache.get(buildCacheKey(filePath, "css"));
- if(cssFile == null) {
- log.error("加载css文件失败,缓存中找不到对应的文件[{}]", filePath);
- }
- return String.format(CSS_SCRIPT, cssFile);
- }
-
- public static void init() throws IOException {
- if(initialized.compareAndSet(false, true)) {
- ServletContext sc = Platform.getInstance().getServletContext();
- if(sc == null) {
- throw new PlatformException("查找静态资源的时候的时候发现servlet context 为null");
- }
- contextPath = Platform.getInstance().getContextPath();
- File wroDirectory = new ServletContextResource(sc, WRO_Path).getFile();
- if(!wroDirectory.exists() || !wroDirectory.isDirectory()) {
- throw new PlatformException("查找静态资源的时候发现对应目录不存在[" + wroDirectory.getAbsolutePath() + "]");
- }
- //将wro目录下已有文件加入缓存
- for(File file : wroDirectory.listFiles()) {
- handleNewFile(file);
- }
- //监控wro目录,若是有文件生成,则判断是不是较新的文件,是的话则把文件名加入缓存
- new Thread(new WroFileWatcher(wroDirectory.getAbsolutePath())).start();
- }
- }
-
- private static void handleNewFile(File file) {
- String fileName = file.getName();
- Pattern p = Pattern.compile("^(\\w+)\\-\\d+\\.(js|css)$");
- Matcher m = p.matcher(fileName);
- if(!m.find() || m.groupCount() < 2) return;
- String fakeName = m.group(1);
- String fileType = m.group(2);
- //暂时限定只能匹配/wro/目录下的文件
- String key = buildCacheKey(WRO_Path + fakeName, fileType);
- if(staticFileCache.putIfAbsent(key, fileName) != null) {
- synchronized(staticFileCache) {
- String cachedFileName = staticFileCache.get(key);
- if(fileName.compareTo(cachedFileName) > 0) {
- staticFileCache.put(key, contextPath + WRO_Path + fileName);
- }
- }
- }
- }
-
- private static String buildCacheKey(String fakeName, String fileType) {
- return fakeName + "-" + fileType;
- }
-
- static class WroFileWatcher implements Runnable {
-
- private static Logger log = LoggerFactory.getLogger(WroFileWatcher.class);
-
- private String wroAbsolutePathStr;
-
- public WroFileWatcher(String wroPathStr) {
- this.wroAbsolutePathStr = wroPathStr;
- }
-
- @Override
- public void run() {
- Path path = Paths.get(wroAbsolutePathStr);
- File wroDirectory = path.toFile();
- if(!wroDirectory.exists() || !wroDirectory.isDirectory()) {
- String message = "监控wro目录的时候发现对应目录不存在[" + wroAbsolutePathStr + "]";
- log.error(message);
- throw new PlatformException(message);
- }
- log.warn("开始监控wro目录[{}]", wroAbsolutePathStr);
- try {
- WatchService watcher = FileSystems.getDefault().newWatchService();
- path.register(watcher, StandardWatchEventKinds.ENTRY_CREATE);
-
- while (true) {
- WatchKey key = null;
- try {
- key = watcher.take();
- } catch (InterruptedException e) {
- log.error("", e);
- continue;
- }
- for (WatchEvent<?> event : key.pollEvents()) {
- if (event.kind() == StandardWatchEventKinds.OVERFLOW) {
- continue;
- }
- WatchEvent<Path> e = (WatchEvent<Path>) event;
- Path filePath = e.context();
- handleNewFile(filePath.toFile());
- }
- if (!key.reset()) {
- break;
- }
- }
- } catch (IOException e) {
- log.error("监控wro目录发生错误", e);
- }
- log.warn("中止监控wro目录[{}]", wroAbsolutePathStr);
- }
- }
- }
对应的tld文件就不给出了,根据方法签名编写就好了.
其中的cssFile和jsFile方法分别实现引用css和js文件.
在页面使用的时候相似这样:
${platform:cssFile("/wro/basic") }
${platform:jsFile("/wro/custom") }
这个类的主要功能就是使用jdk7的WatchService监控wro目录的新增文件事件,
一旦有新的文件加到目录里,判断这个文件是否是最新的,若是是的话则使用这个文件名称引用.
这样一旦有新加的资源文件放到wro目录里,则可以自动被引用,不须要作任何代码上的修改,而且基本不影响性能.
到此为止功能已经实现.
可是我考虑到还有两个问题有待完善:
1.由于生成的文件名称精确到小时,若是这个小时以内有屡次代码修改,生成的文件名都彻底同样.
这样就算线上的代码有修改,对于已经有该文本缓存的浏览器来讲,不会从新请求文件,也就看不到文件变化.
不过通常来讲线上代码不会如此频繁改动,对于大多数应用来讲影响不大.
2.在开发环境开发一段时间以后,wro目录下会生成一大堆的文件(由于m2e-wro4j插件在生成新的文件的时候不会删除旧文件,若是文件名相同会覆盖掉之前的文件),
这时候就须要手动删除时间靠前的旧文件,虽然系统会忽略旧文件,可是我相信大多数程序员和我同样是有些许洁癖的吧.
解决办法仍是很多,好比能够写脚本按期清理掉旧文件.
时间有限,有些地方考虑的不是很完善,欢迎拍砖.
参考资料:
http://meri-stuff.blogspot.sk/2012/08/wro4j-page-load-optimization-and-lessjs.html#Configuration
https://community.jboss.org/en/tools/blog/2012/01/17/css-and-js-minification-using-eclipse-maven-and-wro4j
http://code.google.com/p/wro4j/wiki/MavenPlugin
http://code.google.com/p/wro4j/wiki/WroFileFormat
http://java.dzone.com/articles/using-java-7s-watchservice