使用wro4j和maven在編譯期間壓縮js和css文件

最近在對一個web系統作性能優化.
而對用到的靜態資源文件的壓縮整合則是前端性能優化中很重要的一環.
好處不只在於可以減少請求的文件體積,並且可以減小瀏覽器的http請求數.

由於是基於java的web系統,而且使用的是nginx+tomcat作爲服務器.
最後考慮用wro4j和maven plugin在編譯期間壓縮靜態資源.

優化前:
基本上全部的jsp都引用了這一大坨靜態文件:

Html代碼   收藏代碼
  1. <link rel="stylesheet" type="text/css" href="${ctxPath}/css/skin.css"/>  
  2. <link rel="stylesheet" type="text/css" href="${ctxPath}/css/jquery-ui-1.8.23.custom.css"/>  
  3. <link rel="stylesheet" type="text/css" href="${ctxPath}/css/validationEngine.jquery.css"/>  
  4.   
  5. <script type="text/javascript">var GV = {ctxPath: '${ctxPath}',imgPath: '${ctxPath}/css'};</script>  
  6. <script type="text/javascript" src="${ctxPath}/js/jquery-1.7.2.min.js"></script>  
  7. <script type="text/javascript" src="${ctxPath}/js/jquery-ui-1.8.23.custom.min.js"></script>  
  8. <script type="text/javascript" src="${ctxPath}/js/jquery.validationEngine.js"></script>  
  9. <script type="text/javascript" src="${ctxPath}/js/jquery.validationEngine-zh_CN.js"></script>  
  10. <script type="text/javascript" src="${ctxPath}/js/jquery.fixedtableheader.min.js"></script>  
  11. <script type="text/javascript" src="${ctxPath}/js/roll.js"></script>  
  12. <script type="text/javascript" src="${ctxPath}/js/jquery.pagination.js"></script>  
  13. <script type="text/javascript" src="${ctxPath}/js/jquery.rooFixed.js"></script>  
  14. <script type="text/javascript" src="${ctxPath}/js/jquery.ui.datepicker-zh-CN.js"></script>  
  15. <script type="text/javascript" src="${ctxPath}/js/json2.js"></script>  
  16. <script type="text/javascript" src="${ctxPath}/js/common.js"></script>  


引用的文件不少,而且文件體積沒有壓縮,致使頁面請求的時間很是長.

另外還有一個問題,就是爲了可以充分利用瀏覽器的緩存,靜態資源的文件名稱最好可以作到版本化控制.

這樣前端web服務器就能夠放心大膽的開啓緩存功能而不用擔憂緩存過時問題,由於若是一旦靜態資源文件有修改的話,
會從新生成一個文件名稱.



下面我根據本身項目的經驗,來介紹下如何較好的解決這兩個問題.

分兩步進行.

第一步:引入wro4j,在編譯時期將上述分散的多個文件整合成少數幾個文件,而且將文件最小化.

第二步:在生成的靜態資源文件的文件名稱上加入時間信息


這是兩步優化以後的引用狀況:
Html代碼   收藏代碼
  1. ${platform:cssFile("/wro/basic") }  
  2. <script type="text/javascript">var GV = {ctxPath: '${ctxPath}',imgPath: '${ctxPath}/css'};</script>  
  3. ${platform:jsFile("/wro/basic") }  
  4. ${platform:jsFile("/wro/custom") }  

只引用了1個css文件,2個js文件.http請求從10幾個減小到3個,而且總體文件體積縮小了近一半.

下面介紹優化流程.

第一步:合併而且最小化文件.

1.添加wro4j的maven依賴
Xml代碼   收藏代碼
  1. <wro4j.version>1.6.2</wro4j.version>  
  2.   
  3.    ...  
  4.   
  5.  <dependency>  
  6.   <groupId>ro.isdc.wro4j</groupId>  
  7.   <artifactId>wro4j-core</artifactId>  
  8.   <version>${wro4j.version}</version>  
  9.   <exclusions>  
  10.    <exclusion>  
  11.   
  12.    <!-- 由於項目中的其餘jar包已經引入了不一樣版本的slf4j,因此這裏避免jar重疊因此不引入 -->  
  13.     <groupId>org.slf4j</groupId>  
  14.     <artifactId>slf4j-api</artifactId>  
  15.    </exclusion>  
  16.   </exclusions>  
  17.  </dependency>  


2.添加wro4j maven plugin

Xml代碼   收藏代碼
  1.    <plugin>  
  2.     <groupId>ro.isdc.wro4j</groupId>  
  3.     <artifactId>wro4j-maven-plugin</artifactId>  
  4.     <version>${wro4j.version}</version>  
  5.     <executions>  
  6.      <execution>  
  7.       <phase>compile</phase>  
  8.       <goals>  
  9.        <goal>run</goal>  
  10.       </goals>  
  11.      </execution>  
  12.     </executions>  
  13.     <configuration>  
  14.      <targetGroups>basic,custom</targetGroups>  
  15.   
  16.     <!-- 這個配置是告訴wro4j在打包靜態資源的時候是否須要最小化文件,開發的時候能夠設成false,方便調試 -->  
  17.      <minimize>true</minimize>  
  18.      <destinationFolder>${basedir}/src/main/webapp/wro/</destinationFolder>  
  19.      <contextFolder>${basedir}/src/main/webapp/</contextFolder>  
  20.   
  21. <!-- 這個配置是第二步優化須要用到的,暫時忽略 -->  
  22.      <wroManagerFactory>com.rootrip.platform.common.web.wro.CustomWroManagerFactory</wroManagerFactory>  
  23.     </configuration>  
  24.          </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代碼   收藏代碼
  1. <?xml version="1.0" encoding="UTF-8"?>  
  2. <groups xmlns="http://www.isdc.ro/wro" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"  
  3.  xsi:schemaLocation="http://www.isdc.ro/wro wro.xsd">  
  4.   
  5.  <group name="basic">  
  6.   <css>/css/basic.css</css>  
  7.   <css>/css/skin.css</css>  
  8.   <css>/css/jquery-ui-1.8.23.custom.css</css>  
  9.   <css>/css/validationEngine.jquery.css</css>  
  10.     
  11.   <js>/js/jquery-1.7.2.min.js</js>  
  12.   <js>/js/jquery-ui-1.8.23.custom.min.js</js>  
  13.   <js>/js/jquery.validationEngine.js</js>  
  14.   <js>/js/jquery.fixedtableheader.min.js</js>  
  15.   <js>/js/roll.js</js>  
  16.   <js>/js/jquery.pagination.js</js>  
  17.   <js>/js/jquery.rooFixed.js</js>  
  18.   <js>/js/jquery.ui.datepicker-zh-CN.js</js>  
  19.   <js>/js/json2.js</js>  
  20.  </group>  
  21.    
  22.  <group name="custom">  
  23.   <js>/js/jquery.validationEngine-zh_CN.js</js>  
  24.   <js>/js/common.js</js>  
  25.  </group>  
  26.   
  27. </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代碼   收藏代碼
  1. <link rel="stylesheet" type="text/css" href="${ctxPath}/wro/basic.css"/>  
  2. <script type="text/javascript" src="${ctxPath}/wro/basic.js"></script>  
  3. <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代碼   收藏代碼
  1. public class DailyNamingStrategy extends TimestampNamingStrategy {  
  2.    
  3.  protected final Logger log = LoggerFactory.getLogger(DailyNamingStrategy.class);  
  4.   
  5.  @Override  
  6.  protected long getTimestamp() {  
  7.   String dateStr = DateUtil.formatDate(new Date(), "yyyyMMddHH");  
  8.   return Long.valueOf(dateStr);  
  9.  }  
  10.   
  11.    
  12.   
  13. }  


2.建立CustomWroManagerFactory類

Java代碼   收藏代碼
  1. //這個類就是在wro4j-maven-plugin裏配置的wroManagerFactory參數  
  2. public class CustomWroManagerFactory extends  
  3.   DefaultStandaloneContextAwareManagerFactory {  
  4.  public CustomWroManagerFactory() {  
  5.   setNamingStrategy(new DailyNamingStrategy());  
  6.  }  
  7. }  


上面這兩個類的做用是使用wro4j提供的文件命名策略,這樣生成的文件名就會帶上時間信息了.

例如:basic-2013020217.js

可是如今又會發現一個問題:若是靜態資源文件名稱不固定的話,那怎麼樣引用呢?

這時候就須要經過動態生成<script>與<link>來解決了.

由於項目使用的是jsp頁面,因此經過el自定義函數來實現標籤生成.


3.建立PlatformFunction類

Java代碼   收藏代碼
  1. public class PlatformFunction {  
  2.    
  3.  private static Logger log = LoggerFactory.getLogger(PlatformFunction.class);  
  4.    
  5.    
  6.  private static ConcurrentMap<String, String> staticFileCache = new ConcurrentHashMap<>();  
  7.    
  8.  private static AtomicBoolean initialized = new AtomicBoolean(false);  
  9.    
  10.  private static final String WRO_Path = "/wro/";  
  11.    
  12.  private static final String JS_SCRIPT = "<script type=\"text/javascript\" src=\"%s\"></script>";  
  13.  private static final String CSS_SCRIPT = "<link rel=\"stylesheet\" type=\"text/css\" href=\"%s\">";  
  14.    
  15.  private static String contextPath = null;   
  16.    
  17.  /** 
  18.   * 該方法根據給出的路徑,生成js腳本加載標籤 
  19.   * 例如傳入參數/wro/custom,該方法會尋找webapp路徑下/wro目錄中以custom開頭,以js後綴結尾的文件名稱名稱. 
  20.   * 而後拼成<script type="text/javascript" src="${ctxPath}/wro/custom-20130201.js"></script>返回 
  21.   * 若是查找到多個文件,返回根據文件名排序最大的文件 
  22.   * @param str 
  23.   * @return  
  24.   */  
  25.  public static String jsFile(String filePath) {  
  26.   String jsFile = staticFileCache.get(buildCacheKey(filePath, "js"));  
  27.   if(jsFile == null) {  
  28.    log.error("加載js文件失敗,緩存中找不到對應的文件[{}]", filePath);  
  29.   }  
  30.   return String.format(JS_SCRIPT, jsFile);  
  31.  }  
  32.    
  33.  /** 
  34.   * 該方法根據給出的路徑,生成css腳本加載標籤 
  35.   * 例如傳入參數/wro/custom,該方法會尋找webapp路徑下/wro目錄中以custom開頭,以css後綴結尾的文件名稱名稱. 
  36.   * 而後拼成<link rel="stylesheet" type="text/css" href="${ctxPath}/wro/basic-20130201.css">返回 
  37.   * 若是查找到多個文件,返回根據文件名排序最大的文件 
  38.   * @param str 
  39.   * @return  
  40.   */  
  41.  public static String cssFile(String filePath) {  
  42.   String cssFile = staticFileCache.get(buildCacheKey(filePath, "css"));  
  43.   if(cssFile == null) {  
  44.    log.error("加載css文件失敗,緩存中找不到對應的文件[{}]", filePath);  
  45.   }  
  46.   return String.format(CSS_SCRIPT, cssFile);  
  47.  }  
  48.    
  49.  public static void init() throws IOException {  
  50.   if(initialized.compareAndSet(falsetrue)) {  
  51.    ServletContext sc = Platform.getInstance().getServletContext();  
  52.    if(sc == null) {  
  53.     throw new PlatformException("查找靜態資源的時候的時候發現servlet context 爲null");  
  54.    }  
  55.    contextPath = Platform.getInstance().getContextPath();  
  56.    File wroDirectory = new ServletContextResource(sc, WRO_Path).getFile();  
  57.    if(!wroDirectory.exists() || !wroDirectory.isDirectory()) {  
  58.     throw new PlatformException("查找靜態資源的時候發現對應目錄不存在[" + wroDirectory.getAbsolutePath() + "]");  
  59.    }  
  60.    //將wro目錄下已有文件加入緩存  
  61.    for(File file : wroDirectory.listFiles()) {  
  62.     handleNewFile(file);  
  63.    }  
  64.    //監控wro目錄,若是有文件生成,則判斷是不是較新的文件,是的話則把文件名加入緩存  
  65.    new Thread(new WroFileWatcher(wroDirectory.getAbsolutePath())).start();  
  66.   }  
  67.  }  
  68.   
  69.  private static void handleNewFile(File file) {  
  70.   String fileName = file.getName();  
  71.   Pattern p = Pattern.compile("^(\\w+)\\-\\d+\\.(js|css)$");  
  72.   Matcher m = p.matcher(fileName);  
  73.   if(!m.find() || m.groupCount() < 2return;  
  74.   String fakeName = m.group(1);  
  75.   String fileType = m.group(2);  
  76.   //暫時限定只能匹配/wro/目錄下的文件  
  77.   String key = buildCacheKey(WRO_Path + fakeName, fileType);  
  78.   if(staticFileCache.putIfAbsent(key, fileName) != null) {  
  79.    synchronized(staticFileCache) {  
  80.     String cachedFileName = staticFileCache.get(key);  
  81.     if(fileName.compareTo(cachedFileName) > 0) {  
  82.      staticFileCache.put(key, contextPath + WRO_Path + fileName);  
  83.     }  
  84.    }  
  85.   }  
  86.  }  
  87.    
  88.  private static String buildCacheKey(String fakeName, String fileType) {  
  89.   return fakeName + "-" + fileType;  
  90.  }  
  91.    
  92.  static class WroFileWatcher implements Runnable {  
  93.     
  94.   private static Logger log = LoggerFactory.getLogger(WroFileWatcher.class);  
  95.     
  96.   private String wroAbsolutePathStr;  
  97.     
  98.   public WroFileWatcher(String wroPathStr) {  
  99.    this.wroAbsolutePathStr = wroPathStr;  
  100.   }  
  101.   
  102.   @Override  
  103.   public void run() {  
  104.    Path path = Paths.get(wroAbsolutePathStr);  
  105.    File wroDirectory = path.toFile();  
  106.    if(!wroDirectory.exists() || !wroDirectory.isDirectory()) {  
  107.     String message = "監控wro目錄的時候發現對應目錄不存在[" + wroAbsolutePathStr + "]";  
  108.     log.error(message);  
  109.     throw new PlatformException(message);  
  110.    }  
  111.    log.warn("開始監控wro目錄[{}]", wroAbsolutePathStr);  
  112.    try {  
  113.     WatchService watcher = FileSystems.getDefault().newWatchService();  
  114.     path.register(watcher, StandardWatchEventKinds.ENTRY_CREATE);  
  115.       
  116.     while (true) {  
  117.      WatchKey key = null;  
  118.      try {  
  119.       key = watcher.take();  
  120.      } catch (InterruptedException e) {  
  121.       log.error("", e);  
  122.       continue;  
  123.      }  
  124.      for (WatchEvent<?> event : key.pollEvents()) {  
  125.       if (event.kind() == StandardWatchEventKinds.OVERFLOW) {  
  126.        continue;  
  127.       }  
  128.       WatchEvent<Path> e = (WatchEvent<Path>) event;  
  129.       Path filePath = e.context();  
  130.       handleNewFile(filePath.toFile());  
  131.      }  
  132.      if (!key.reset()) {  
  133.       break;  
  134.      }  
  135.     }  
  136.    } catch (IOException e) {  
  137.     log.error("監控wro目錄發生錯誤", e);  
  138.    }  
  139.    log.warn("中止監控wro目錄[{}]", wroAbsolutePathStr);  
  140.   }  
  141.  }  
  142. }  


對應的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
相關文章
相關標籤/搜索