最近在對一個web系統(tǒng)做性能優(yōu)化.
而對用到的靜態(tài)資源文件的壓縮整合則是前端性能優(yōu)化中很重要的一環(huán).
好處不僅在于能夠減小請求的文件體積,而且能夠減少瀏覽器的http請求數(shù).
因為是基于java的web系統(tǒng),并且使用的是nginx+tomcat做為服務(wù)器.
最后考慮用wro4j和maven plugin在編譯期間壓縮靜態(tài)資源.
優(yōu)化前:
基本上所有的jsp都引用了這一大坨靜態(tài)文件:
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>
引用的文件很多,并且文件體積沒有壓縮,導(dǎo)致頁面請求的時間非常長.
另外還有一個問題,就是為了能夠充分利用瀏覽器的緩存,靜態(tài)資源的文件名稱最好能夠做到版本化控制.
這樣前端web服務(wù)器就可以放心大膽的開啟緩存功能而不用擔(dān)心緩存過期問題,因為如果一旦靜態(tài)資源文件有修改的話,
會重新生成一個文件名稱.
下面我根據(jù)自己項目的經(jīng)驗,來介紹下如何較好的解決這兩個問題.
分兩步進行.
第一步:引入wro4j,在編譯時期將上述分散的多個文件整合成少數(shù)幾個文件,并且將文件最小化.
第二步:在生成的靜態(tài)資源文件的文件名稱上加入時間信息
這是兩步優(yōu)化之后的引用情況:
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個,并且整體文件體積縮小了近一半.
下面介紹優(yōu)化流程.
第一步:合并并且最小化文件.
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包已經(jīng)引入了不同版本的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在打包靜態(tài)資源的時候是否需要最小化文件,開發(fā)的時候可以設(shè)成false,方便調(diào)試 -->
<minimize>true</minimize>
<destinationFolder>${basedir}/src/main/webapp/wro/</destinationFolder>
<contextFolder>${basedir}/src/main/webapp/</contextFolder>
<!-- 這個配置是第二步優(yōu)化需要用到的,暫時忽略 -->
<wroManagerFactory>com.rootrip.platform.common.web.wro.CustomWroManagerFactory</wroManagerFactory>
</configuration>
</plugin>
如果開發(fā)環(huán)境是eclipse的話,可以下載m2e-wro4j這個插件.
下載地址:http://download.jboss.org/jbosstools/updates/m2e-wro4j/
這個插件的主要功能是能夠幫助我們在開發(fā)環(huán)境下修改對應(yīng)的靜態(tài)文件,或者pom.xml文件的時候能夠自動生成打包好的js和css文件.
對開發(fā)來說就會方便很多.只要修改源文件就能看見修改后的結(jié)果.
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的文件到指定目錄.
第一步搞定,這時候如果你的開發(fā)環(huán)境是eclipse并且安裝了插件的話,應(yīng)該就能在你工程的%your webapp%/wor/目錄下看見生成好的
basic.css,basic.js和custom.js這三個文件了.
然后你再將你的靜態(tài)資源引用路徑改成
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文件的時候,這些文件都將重新生成.
如果開發(fā)環(huán)境是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自定義函數(shù)引用腳本文件.
1. 創(chuàng)建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.創(chuàng)建CustomWroManagerFactory類
Java代碼 收藏代碼
//這個類就是在wro4j-maven-plugin里配置的wroManagerFactory參數(shù)
public class CustomWroManagerFactory extends
DefaultStandaloneContextAwareManagerFactory {
public CustomWroManagerFactory() {
setNamingStrategy(new DailyNamingStrategy());
}
}
上面這兩個類的作用是使用wro4j提供的文件命名策略,這樣生成的文件名就會帶上時間信息了.
例如:basic-2013020217.js
但是現(xiàn)在又會發(fā)現(xiàn)一個問題:如果靜態(tài)資源文件名稱不固定的話,那怎么樣引用呢?
這時候就需要通過動態(tài)生成<script>與<link>來解決了.
因為項目使用的是jsp頁面,所以通過el自定義函數(shù)來實現(xiàn)標(biāo)簽生成.
3.創(chuàng)建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;
/**
* 該方法根據(jù)給出的路徑,生成js腳本加載標(biāo)簽
* 例如傳入?yún)?shù)/wro/custom,該方法會尋找webapp路徑下/wro目錄中以custom開頭,以js后綴結(jié)尾的文件名稱名稱.
* 然后拼成<script type="text/javascript" src="${ctxPath}/wro/custom-20130201.js"></script>返回
* 如果查找到多個文件,返回根據(jù)文件名排序最大的文件
* @param str
* @return
*/
public static String jsFile(String filePath) {
String jsFile = staticFileCache.get(buildCacheKey(filePath, "js"));
if(jsFile == null) {
log.error("加載js文件失敗,緩存中找不到對應(yīng)的文件[{}]", filePath);
}
return String.format(JS_SCRIPT, jsFile);
}
/**
* 該方法根據(jù)給出的路徑,生成css腳本加載標(biāo)簽
* 例如傳入?yún)?shù)/wro/custom,該方法會尋找webapp路徑下/wro目錄中以custom開頭,以css后綴結(jié)尾的文件名稱名稱.
* 然后拼成<link rel="stylesheet" type="text/css" href="${ctxPath}/wro/basic-20130201.css">返回
* 如果查找到多個文件,返回根據(jù)文件名排序最大的文件
* @param str
* @return
*/
public static String cssFile(String filePath) {
String cssFile = staticFileCache.get(buildCacheKey(filePath, "css"));
if(cssFile == null) {
log.error("加載css文件失敗,緩存中找不到對應(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)資源的時候的時候發(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)資源的時候發(fā)現(xiàn)對應(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);
//暫時限定只能匹配/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目錄的時候發(fā)現(xiàn)對應(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ā)生錯誤", e);
}
log.warn("停止監(jiān)控wro目錄[{}]", wroAbsolutePathStr);
}
}
}
對應(yīng)的tld文件就不給出了,根據(jù)方法簽名編寫就行了.
其中的cssFile和jsFile方法分別實現(xiàn)引用css和js文件.
在頁面使用的時候類似這樣:
${platform:cssFile("/wro/basic") }
${platform:jsFile("/wro/custom") }
這個類的主要功能就是使用jdk7的WatchService監(jiān)控wro目錄的新增文件事件,
一旦有新的文件加到目錄里,判斷這個文件是不是最新的,如果是的話則使用這個文件名稱引用.
這樣一旦有新加的資源文件放到wro目錄里,則能夠自動被引用,不需要做任何代碼上的修改,并且基本不影響性能.
到此為止功能已經(jīng)實現(xiàn).
但是我考慮到還有兩個問題有待完善:
1.因為生成的文件名稱精確到小時,如果這個小時之內(nèi)有多次代碼修改,生成的文件名都完全一樣.
這樣就算線上的代碼有修改,對于已經(jīng)有該文本緩存的瀏覽器來說,不會重新請求文件,也就看不到文件變化.
不過一般來說線上代碼不會如此頻繁改動,對于大多數(shù)應(yīng)用來說影響不大.
2.在開發(fā)環(huán)境開發(fā)一段時間之后,wro目錄下會生成一大堆的文件(因為m2e-wro4j插件在生成新的文件的時候不會刪除舊文件,如果文件名相同會覆蓋掉以前的文件),
這時候就需要手動刪除時間靠前的舊文件,雖然系統(tǒng)會忽略舊文件,但是我相信大多數(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
本站僅提供存儲服務(wù),所有內(nèi)容均由用戶發(fā)布,如發(fā)現(xiàn)有害或侵權(quán)內(nèi)容,請
點擊舉報。