最近在對(duì)一個(gè)web系統(tǒng)做性能優(yōu)化.
而對(duì)用到的靜態(tài)資源文件的壓縮整合則是前端性能優(yōu)化中很重要的一環(huán).
好處不僅在于能夠減小請(qǐng)求的文件體積,而且能夠減少瀏覽器的http請(qǐng)求數(shù).
因?yàn)槭腔趈ava的web系統(tǒng),并且使用的是nginx+tomcat做為服務(wù)器.
最后考慮用wro4j和maven plugin在編譯期間壓縮靜態(tài)資源.
優(yōu)化前:
基本上所有的jsp都引用了這一大坨靜態(tài)文件:
- <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>
引用的文件很多,并且文件體積沒有壓縮,導(dǎo)致頁面請(qǐng)求的時(shí)間非常長(zhǎng).
另外還有一個(gè)問題,就是為了能夠充分利用瀏覽器的緩存,靜態(tài)資源的文件名稱最好能夠做到版本化控制.
這樣前端web服務(wù)器就可以放心大膽的開啟緩存功能而不用擔(dān)心緩存過期問題,因?yàn)槿绻坏╈o態(tài)資源文件有修改的話,
會(huì)重新生成一個(gè)文件名稱.
下面我根據(jù)自己項(xiàng)目的經(jīng)驗(yàn),來介紹下如何較好的解決這兩個(gè)問題.
分兩步進(jìn)行.
第一步:引入wro4j,在編譯時(shí)期將上述分散的多個(gè)文件整合成少數(shù)幾個(gè)文件,并且將文件最小化.
第二步:在生成的靜態(tài)資源文件的文件名稱上加入時(shí)間信息
這是兩步優(yōu)化之后的引用情況:
- ${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個(gè)css文件,2個(gè)js文件.http請(qǐng)求從10幾個(gè)減少到3個(gè),并且整體文件體積縮小了近一半.
下面介紹優(yōu)化流程.
第一步:合并并且最小化文件.
1.添加wro4j的maven依賴
- <wro4j.version>1.6.2</wro4j.version>
-
- ...
-
- <dependency>
- <groupId>ro.isdc.wro4j</groupId>
- <artifactId>wro4j-core</artifactId>
- <version>${wro4j.version}</version>
- <exclusions>
- <exclusion>
-
- <!-- 因?yàn)轫?xiàng)目中的其他jar包已經(jīng)引入了不同版本的slf4j,所以這里避免jar重疊所以不引入 -->
- <groupId>org.slf4j</groupId>
- <artifactId>slf4j-api</artifactId>
- </exclusion>
- </exclusions>
- </dependency>
2.添加wro4j maven plugin
- <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>
-
- <!-- 這個(gè)配置是告訴wro4j在打包靜態(tài)資源的時(shí)候是否需要最小化文件,開發(fā)的時(shí)候可以設(shè)成false,方便調(diào)試 -->
- <minimize>true</minimize>
- <destinationFolder>${basedir}/src/main/webapp/wro/</destinationFolder>
- <contextFolder>${basedir}/src/main/webapp/</contextFolder>
-
- <!-- 這個(gè)配置是第二步優(yōu)化需要用到的,暫時(shí)忽略 -->
- <wroManagerFactory>com.rootrip.platform.common.web.wro.CustomWroManagerFactory</wroManagerFactory>
- </configuration>
- </plugin>
如果開發(fā)環(huán)境是eclipse的話,可以下載m2e-wro4j這個(gè)插件.
下載地址:http://download.jboss.org/jbosstools/updates/m2e-wro4j/
這個(gè)插件的主要功能是能夠幫助我們?cè)陂_發(fā)環(huán)境下修改對(duì)應(yīng)的靜態(tài)文件,或者pom.xml文件的時(shí)候能夠自動(dòng)生成打包好的js和css文件.
對(duì)開發(fā)來說就會(huì)方便很多.只要修改源文件就能看見修改后的結(jié)果.
3.在WEB-INF目錄下添加wro.xml文件,這個(gè)文件的作用就是告訴wro4j需要以怎樣的策略打包jss和css文件.
- <?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
其實(shí)這個(gè)配置文件很好理解,如果不愿看官方文檔的朋友我在這簡(jiǎn)單介紹下.
上面這樣配置的目的就是告訴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>
這四個(gè)文件整合到一起,生成一個(gè)叫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>
這幾個(gè)文件整合到一起,生成一個(gè)叫basic.js的文件到指定目錄.
最后將
<js>/js/jquery.validationEngine-zh_CN.js</js>
<js>/js/common.js</js>
這兩個(gè)文件整合到一起,,生成一個(gè)叫custom.js的文件到指定目錄.
第一步搞定,這時(shí)候如果你的開發(fā)環(huán)境是eclipse并且安裝了插件的話,應(yīng)該就能在你工程的%your webapp%/wor/目錄下看見生成好的
basic.css,basic.js和custom.js這三個(gè)文件了.
然后你再將你的靜態(tài)資源引用路徑改成
- <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文件的時(shí)候,這些文件都將重新生成.
如果開發(fā)環(huán)境是eclipse但是沒有安裝m2e-wro4j插件的話,pom.xml可能需要額外配置.
請(qǐng)參考: https://community.jboss.org/en/tools/blog/2012/01/17/css-and-js-minification-using-eclipse-maven-and-wro4j
第二步:給生成的文件名稱中加入時(shí)間信息并通過el自定義函數(shù)引用腳本文件.
1. 創(chuàng)建DailyNamingStrategy類
- 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.創(chuàng)建CustomWroManagerFactory類
- //這個(gè)類就是在wro4j-maven-plugin里配置的wroManagerFactory參數(shù)
- public class CustomWroManagerFactory extends
- DefaultStandaloneContextAwareManagerFactory {
- public CustomWroManagerFactory() {
- setNamingStrategy(new DailyNamingStrategy());
- }
- }
上面這兩個(gè)類的作用是使用wro4j提供的文件命名策略,這樣生成的文件名就會(huì)帶上時(shí)間信息了.
例如:basic-2013020217.js
但是現(xiàn)在又會(huì)發(fā)現(xiàn)一個(gè)問題:如果靜態(tài)資源文件名稱不固定的話,那怎么樣引用呢?
這時(shí)候就需要通過動(dòng)態(tài)生成<script>與<link>來解決了.
因?yàn)轫?xiàng)目使用的是jsp頁面,所以通過el自定義函數(shù)來實(shí)現(xiàn)標(biāo)簽生成.
3.創(chuàng)建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;
-
- /**
- * 該方法根據(jù)給出的路徑,生成js腳本加載標(biāo)簽
- * 例如傳入?yún)?shù)/wro/custom,該方法會(huì)尋找webapp路徑下/wro目錄中以custom開頭,以js后綴結(jié)尾的文件名稱名稱.
- * 然后拼成<script type="text/javascript" src="${ctxPath}/wro/custom-20130201.js"></script>返回
- * 如果查找到多個(gè)文件,返回根據(jù)文件名排序最大的文件
- * @param str
- * @return
- */
- public static String jsFile(String filePath) {
- String jsFile = staticFileCache.get(buildCacheKey(filePath, "js"));
- if(jsFile == null) {
- log.error("加載js文件失敗,緩存中找不到對(duì)應(yīng)的文件[{}]", filePath);
- }
- return String.format(JS_SCRIPT, jsFile);
- }
-
- /**
- * 該方法根據(jù)給出的路徑,生成css腳本加載標(biāo)簽
- * 例如傳入?yún)?shù)/wro/custom,該方法會(huì)尋找webapp路徑下/wro目錄中以custom開頭,以css后綴結(jié)尾的文件名稱名稱.
- * 然后拼成<link rel="stylesheet" type="text/css" href="${ctxPath}/wro/basic-20130201.css">返回
- * 如果查找到多個(gè)文件,返回根據(jù)文件名排序最大的文件
- * @param str
- * @return
- */
- public static String cssFile(String filePath) {
- String cssFile = staticFileCache.get(buildCacheKey(filePath, "css"));
- if(cssFile == null) {
- log.error("加載css文件失敗,緩存中找不到對(duì)應(yīng)的文件[{}]", 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("查找靜態(tài)資源的時(shí)候的時(shí)候發(fā)現(xiàn)servlet context 為null");
- }
- contextPath = Platform.getInstance().getContextPath();
- File wroDirectory = new ServletContextResource(sc, WRO_Path).getFile();
- if(!wroDirectory.exists() || !wroDirectory.isDirectory()) {
- throw new PlatformException("查找靜態(tài)資源的時(shí)候發(fā)現(xiàn)對(duì)應(yīng)目錄不存在[" + wroDirectory.getAbsolutePath() + "]");
- }
- //將wro目錄下已有文件加入緩存
- for(File file : wroDirectory.listFiles()) {
- handleNewFile(file);
- }
- //監(jiān)控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);
- //暫時(shí)限定只能匹配/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 = "監(jiān)控wro目錄的時(shí)候發(fā)現(xiàn)對(duì)應(yīng)目錄不存在[" + wroAbsolutePathStr + "]";
- log.error(message);
- throw new PlatformException(message);
- }
- log.warn("開始監(jiān)控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("監(jiān)控wro目錄發(fā)生錯(cuò)誤", e);
- }
- log.warn("停止監(jiān)控wro目錄[{}]", wroAbsolutePathStr);
- }
- }
- }
對(duì)應(yīng)的tld文件就不給出了,根據(jù)方法簽名編寫就行了.
其中的cssFile和jsFile方法分別實(shí)現(xiàn)引用css和js文件.
在頁面使用的時(shí)候類似這樣:
${platform:cssFile("/wro/basic") }
${platform:jsFile("/wro/custom") }
這個(gè)類的主要功能就是使用jdk7的WatchService監(jiān)控wro目錄的新增文件事件,
一旦有新的文件加到目錄里,判斷這個(gè)文件是不是最新的,如果是的話則使用這個(gè)文件名稱引用.
這樣一旦有新加的資源文件放到wro目錄里,則能夠自動(dòng)被引用,不需要做任何代碼上的修改,并且基本不影響性能.
到此為止功能已經(jīng)實(shí)現(xiàn).
但是我考慮到還有兩個(gè)問題有待完善:
1.因?yàn)樯傻奈募Q精確到小時(shí),如果這個(gè)小時(shí)之內(nèi)有多次代碼修改,生成的文件名都完全一樣.
這樣就算線上的代碼有修改,對(duì)于已經(jīng)有該文本緩存的瀏覽器來說,不會(huì)重新請(qǐng)求文件,也就看不到文件變化.
不過一般來說線上代碼不會(huì)如此頻繁改動(dòng),對(duì)于大多數(shù)應(yīng)用來說影響不大.
2.在開發(fā)環(huán)境開發(fā)一段時(shí)間之后,wro目錄下會(huì)生成一大堆的文件(因?yàn)閙2e-wro4j插件在生成新的文件的時(shí)候不會(huì)刪除舊文件,如果文件名相同會(huì)覆蓋掉以前的文件),
這時(shí)候就需要手動(dòng)刪除時(shí)間靠前的舊文件,雖然系統(tǒng)會(huì)忽略舊文件,但是我相信大多數(shù)程序員和我一樣是有些許潔癖的吧.
解決辦法還是不少,比如可以寫腳本定期清理掉舊文件.
時(shí)間有限,有些地方考慮的不是很完善,歡迎拍磚.
參考資料:
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