前端js和css的压缩合并之wro4j

me:注:

找了一圈,在java社区和node社区,就没有找到中意的解决方案,总是带有各种各样的毛病。

看来ror社区还是拥有最强悍的设计和构架能力的,也许得益于ruby的精神。

最牛牛的人有时候只要一两个就够了。

下面的文还是不错的,可惜wro4j的愚笨复杂设计啊。

from:使用wro4j和maven在编译期间压缩js和css文件

最近在对一个web系统做性能优化. 
而对用到的静态资源文件的压缩整合则是前端性能优化中很重要的一环. 
好处不仅在于能够减小请求的文件体积,而且能够减少浏览器的http请求数. 

因为是基于java的web系统,并且使用的是nginx+tomcat做为服务器. 
最后考虑用wro4j和maven plugin在编译期间压缩静态资源. 

优化前: 
基本上所有的jsp都引用了这一大坨静态文件: 

<?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这三个文件了. 

然后你再将你的静态资源引用路径改成 

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类 

//这个类就是在wro4j-maven-plugin里配置的wroManagerFactory参数  
public class CustomWroManagerFactory extends  
  DefaultStandaloneContextAwareManagerFactory {  
 public CustomWroManagerFactory() {  
  setNamingStrategy(new DailyNamingStrategy());  
 }  
}  



上面这两个类的作用是使用wro4j提供的文件命名策略,这样生成的文件名就会带上时间信息了. 

例如:basic-2013020217.js 

但是现在又会发现一个问题:如果静态资源文件名称不固定的话,那怎么样引用呢? 

这时候就需要通过动态生成<script>与<link>来解决了. 

因为项目使用的是jsp页面,所以通过el自定义函数来实现标签生成. 


3.创建PlatformFunction类 

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 

+

+
+
-

+
+
+

相关推荐