YUI3中,为了避免js文件过大,各个功能模块是拆分的。它有一个“种子”的概念:先下载一个小的核心的js文件到浏览器端,再通过这个小的js文件去加载其它所需的模块。
这种按需加载虽然解决了单个js过大的问题,但是随之带来另外一个问题:如果一个页面使用了YUI的a、b、c功能,那么浏览器就要向服务器请求a.js、b.js、c.js三个文件,这样增加了浏览器向服务器的沟通次数。
为了解决后面的问题,YUI3又有一个combine的概念,预先下载的那个核心的js,把页面上需要的a、b、c模块合并成一个请求发给服务器,类似这样:http://mydomain.com/conbine?a.js&b.js&c.js。
这要求服务器接收到http://mydomain.com/conbine请求后,将参数取出来,找到对应的a、b、c的js文件,合并成一个js文件,返回给客户端。Combo Handler是Yahoo!开发的一个Apache模块,专门来干这个事情的。
如果只用java的web容器,可以把这项工作委托给servlet:
<servlet>
<servlet-name>yuicombo</servlet-name>
<servlet-class>org.siqisource.mozo.servlets.YuiCombineServlet</servlet-class>
</servlet>
对应的YUI配置为:
YUI.GlobalConfig = {
combine: true,
comboBase: '<mz:webRoot/>/yuicombo?',
root: 'uilibrary/yui/',
};
在servlet代码中,我使用了YUI的yuicompressor来压缩js和css文件,下面是maven配置。
<dependency>
<groupId>com.yahoo.platform.yui</groupId>
<artifactId>yuicompressor</artifactId>
<version>2.4.7</version>
</dependency>
其中的Path类只是为了获得web应用物理路径,在是用的时候替换一下即可。
目前已知的缺陷:对于css的按需加载,浏览器会请求客户端两次,目前不清楚是不是YUI3(测试版本:3.7.2)的问题。
具体代码如下:
package org.siqisource.mozo.servlets;
import java.io.File;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;
import java.io.Reader;
import java.io.Writer;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.apache.commons.io.FileDeleteStrategy;
import org.apache.commons.io.FileUtils;
import org.siqisource.mozo.context.Path;
import com.yahoo.platform.yui.compressor.CssCompressor;
import com.yahoo.platform.yui.compressor.JavaScriptCompressor;
public class YuiCombineServlet extends HttpServlet {
private static Map<String, String> cachedResources = new HashMap<String, String>();
private String cacheContextPath = "uilibrary/yui/cache/";
private String cacheDir = Path.getPhysicalPath() + cacheContextPath;
int linebreakpos = -1;
boolean munge = true;
boolean verbose = false;
boolean preserveAllSemiColons = false;
boolean disableOptimizations = false;
@Override
public void init() throws ServletException {
// 重置缓存文件夹
File catchedDir = new File(cacheDir);
if (catchedDir.exists()) {
FileDeleteStrategy strategy = FileDeleteStrategy.FORCE;
try {
strategy.delete(catchedDir);
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
catchedDir.mkdirs();
super.init();
}
@Override
protected void doGet(HttpServletRequest request,
HttpServletResponse response) throws ServletException, IOException {
String queryString = request.getQueryString();
String resourcePath = cachedResources.get(queryString);
// 已缓存
if (resourcePath == null) {
String[] resources = queryString.split("&");
String firstResource = resources[0];
String fileName = UUID.randomUUID().toString();
if (firstResource.endsWith(".js")) {
fileName += ".js";
Writer writer = new FileWriter(cacheDir + fileName);
for (String resource : resources) {
Reader reader = new FileReader(Path.getPhysicalPath()
+ resource);
JavaScriptCompressor compressor = new JavaScriptCompressor(
reader, null);
compressor.compress(writer, linebreakpos, munge, verbose,
preserveAllSemiColons, disableOptimizations);
reader.close();
}
writer.flush();
writer.close();
} else if (resources[0].endsWith(".css")) {
fileName += ".css";
Writer writer = new FileWriter(cacheDir + fileName);
for (String resource : resources) {
Reader reader = new FileReader(replacedUrlFile(resource));
CssCompressor compressor = new CssCompressor(reader);
compressor.compress(writer, linebreakpos);
reader.close();
}
writer.flush();
writer.close();
}
resourcePath = cacheContextPath + fileName;
cachedResources.put(queryString, resourcePath);
}
request.getRequestDispatcher(resourcePath).forward(request, response);
return;
}
public String replacedUrlFile(String fileName) throws IOException {
String cssfilePath = Path.getPhysicalPath() + fileName;
File cssFile = new File(cssfilePath);
String tempCssFilePath = cacheDir + "tmp-css-" + cssFile.getName();
File tempCssFile = new File(tempCssFilePath);
if (tempCssFile.exists()) {
return tempCssFilePath;
}
// 判断是否需要替换
String css = FileUtils.readFileToString(cssFile);
int maxIndex = css.length() - 1;
int appendIndex = 0;
Pattern p = Pattern.compile("url\\(\\s*([\"']?)");
if (!p.matcher(css).find()) {
return cssfilePath;
}
// 真的需要替换
Matcher m = p.matcher(css);
String url = fileName.substring(0, fileName.lastIndexOf('/'));
url = Path.getContextPath() + "/" + url + "/";
StringBuffer replacedUrlCss = new StringBuffer();
while (m.find()) {
int startIndex = m.start() + 4; // "url(".length()
String terminator = m.group(1); // ', " or empty (not quoted)
if (terminator.length() == 0) {
terminator = ")";
}
boolean foundTerminator = false;
int endIndex = m.end() - 1;
while (foundTerminator == false && endIndex + 1 <= maxIndex) {
endIndex = css.indexOf(terminator, endIndex + 1);
if ((endIndex > 0) && (css.charAt(endIndex - 1) != '\\')) {
foundTerminator = true;
if (!")".equals(terminator)) {
endIndex = css.indexOf(")", endIndex);
}
}
}
// Enough searching, start moving stuff over to the buffer
replacedUrlCss.append(css.substring(appendIndex, m.start()));
if (foundTerminator) {
String token = css.substring(startIndex, endIndex);
token = token.replaceAll("\\s+", "");
String preserver = "url('" + url + token + "')";
replacedUrlCss.append(preserver);
appendIndex = endIndex + 1;
} else {
// No end terminator found, re-add the whole match. Should we
// throw/warn here?
replacedUrlCss.append(css.substring(m.start(), m.end()));
appendIndex = m.end();
}
}
FileUtils.writeStringToFile(tempCssFile, replacedUrlCss.toString());
return tempCssFilePath;
}
}