大家都知道,在Web应用程序中,为了节省网络开销,往往吧多个小的js文件整合成一个大的js文件,吧多个小的css文件整合成一个大的js文件,这样原本N次小文件的请求就可以合并成单次的网络请求。最典型的做这件事情的工具是大名鼎鼎的yui-compressor.
其实在Liferay中,我们为了达到合并css,js的目的,用了不同于yui-compressor的方法,这就是我们的主角 MinifierFilter.
既然是Filter,那么它肯定有filter-mapping,我们轻易的在liferay-web.xml中找到了Filter的定义和Filter的mapping.
- ...
- <filter>
- <filter-name>Minifier Filter</filter-name>
- <filter-class>com.liferay.portal.servlet.filters.minifier.MinifierFilter</filter-class>
- </filter>
- <filter> <filter-name>Minifier Filter - JSP</filter-name> <filter-class>com.liferay.portal.servlet.filters.minifier.MinifierFilter</filter-class> <init-param> <param-name>url-regex-pattern</param-name> <param-value>.+/(aui_lang|barebone|css|everything|main)\.jsp</param-value> </init-param> </filter>
- ...
- <filter-mapping>
- <filter-name>Minifier Filter</filter-name>
- <url-pattern>*.css</url-pattern>
- </filter-mapping>
- <filter-mapping>
- <filter-name>Minifier Filter</filter-name>
- <url-pattern>*.js</url-pattern>
- </filter-mapping>
- <filter-mapping> <filter-name>Minifier Filter - JSP</filter-name> <url-pattern>*.jsp</url-pattern> </filter-mapping>
- ...
所以,我们可以看到,当客户端对Liferay服务器上请求任意css或者javascript资源时候,都会被这个MinifierFilter所过滤,我们现在就来看下庐山真面目。
因为MinifierFilter最终实现了Filter接口,而doFilter方法在父类的父类BaseFilter中定义,这个doFilter仅仅是调用processFilter()方法,
- public void doFilter(
- ServletRequest servletRequest, ServletResponse servletResponse,
- FilterChain filterChain)
- throws IOException, ServletException {
- try {
- HttpServletRequest request = (HttpServletRequest)servletRequest;
- HttpServletResponse response = (HttpServletResponse)servletResponse;
- processFilter(request, response, filterChain);
- }
- catch (IOException ioe) {
- throw ioe;
- }
- catch (ServletException se) {
- throw se;
- }
所以这就是我们的入口:
- protected void processFilter(
- HttpServletRequest request, HttpServletResponse response,
- FilterChain filterChain)
- throws Exception {
- Object minifiedContent = getMinifiedContent(
- request, response, filterChain);
- if (minifiedContent == null) {
- minifiedContent = getMinifiedBundleContent(request, response);
- }
- if (minifiedContent == null) {
- processFilter(MinifierFilter.class, request, response, filterChain);
- }
- else {
- if (minifiedContent instanceof File) {
- ServletResponseUtil.write(response, (File)minifiedContent);
- }
- else if (minifiedContent instanceof String) {
- ServletResponseUtil.write(response, (String)minifiedContent);
- }
- }
- }
首先,它会去执行06-07行的getMinifiedContent()方法,它会调用以下代码:
在getMinifiedContent()方法中,它会调用2个方法来分别最小化css和最小化js.
如下:
- protected Object getMinifiedContent(
- HttpServletRequest request, HttpServletResponse response,
- FilterChain filterChain)
- throws Exception {
- ..
- ..
- String minifiedContent = null;
- if (realPath.endsWith(_CSS_EXTENSION)) {
- if (_log.isInfoEnabled()) {
- _log.info("Minifying CSS " + file);
- }
- minifiedContent = minifyCss(request, response, file);
- response.setContentType(ContentTypes.TEXT_CSS);
- FileUtil.write(cacheContentTypeFile, ContentTypes.TEXT_CSS);
- }
- else if (realPath.endsWith(_JAVASCRIPT_EXTENSION)) {
- if (_log.isInfoEnabled()) {
- _log.info("Minifying JavaScript " + file);
- }
- minifiedContent = minifyJavaScript(file);
- response.setContentType(ContentTypes.TEXT_JAVASCRIPT);
- FileUtil.write(cacheContentTypeFile, ContentTypes.TEXT_JAVASCRIPT);
- }
- else if (realPath.endsWith(_JSP_EXTENSION)) {
- if (_log.isInfoEnabled()) {
- _log.info("Minifying JSP " + file);
- }
- StringServletResponse stringResponse = new StringServletResponse(
- response);
- processFilter(
- MinifierFilter.class, request, stringResponse, filterChain);
- CacheResponseUtil.setHeaders(response, stringResponse.getHeaders());
- response.setContentType(stringResponse.getContentType());
- minifiedContent = stringResponse.getString();
- if (minifierType.equals("css")) {
- minifiedContent = minifyCss(
- request, response, realPath, minifiedContent);
- }
- else if (minifierType.equals("js")) {
- minifiedContent = minifyJavaScript(minifiedContent);
- }
- FileUtil.write(
- cacheContentTypeFile, stringResponse.getContentType());
- }
- else {
- return null;
- }
- FileUtil.write(cacheDataFile, minifiedContent);
- return minifiedContent;
- }
minifyCSS:
从第12行可以看出,如果判断扩展名是.css,那么需要吧文件minify一下,并且设置content-type为text/css,最后把这个文件放入cacheContentTypeFile中。
我们来看下minifyCSS到底做了什么事情:
- protected String minifyCss(
- HttpServletRequest request, HttpServletResponse response, File file)
- throws IOException {
- String content = FileUtil.read(file);
- content = aggregateCss(file.getParent(), content);
- return minifyCss(request, response, file.getAbsolutePath(), content);
- }
从这里可以清楚的看出,
05行是先吧这个css文件的内容通过FileUtil读出来,其实这个FileUtil的读的方式会去除所有的换行,参见最终调用的FileImpl的read方法:
- public String read(File file, boolean raw) throws IOException {
- byte[] bytes = getBytes(file);
- if (bytes == null) {
- return null;
- }
- String s = new String(bytes, StringPool.UTF8);
- if (raw) {
- return s;
- }
- else {
- return StringUtil.replace(
- s, StringPool.RETURN_NEW_LINE, StringPool.NEW_LINE);
- }
- }
然后把去除了所有换行的css文件的内容存入到变量content中。
07行会调用aggregateCSS来对这个css文件的内容做进一步处理,如何处理呢,我们看代码:
- public static String aggregateCss(String dir, String content)
- throws IOException {
- StringBuilder sb = new StringBuilder(content.length());
- int pos = 0;
- while (true) {
- int commentX = content.indexOf(_CSS_COMMENT_BEGIN, pos);
- int commentY = content.indexOf(
- _CSS_COMMENT_END, commentX + _CSS_COMMENT_BEGIN.length());
- int importX = content.indexOf(_CSS_IMPORT_BEGIN, pos);
- int importY = content.indexOf(
- _CSS_IMPORT_END, importX + _CSS_IMPORT_BEGIN.length());
- if ((importX == -1) || (importY == -1)) {
- sb.append(content.substring(pos, content.length()));
- break;
- }
- else if ((commentX != -1) && (commentY != -1) &&
- (commentX < importX) && (commentY > importX)) {
- commentY += _CSS_COMMENT_END.length();
- sb.append(content.substring(pos, commentY));
- pos = commentY;
- }
- else {
- sb.append(content.substring(pos, importX));
- String importFileName = content.substring(
- importX + _CSS_IMPORT_BEGIN.length(), importY);
- String importFullFileName = dir.concat(StringPool.SLASH).concat(
- importFileName);
- String importContent = FileUtil.read(importFullFileName);
- if (importContent == null) {
- if (_log.isWarnEnabled()) {
- _log.warn(
- "File " + importFullFileName + " does not exist");
- }
- importContent = StringPool.BLANK;
- }
- String importDir = StringPool.BLANK;
- int slashPos = importFileName.lastIndexOf(CharPool.SLASH);
- if (slashPos != -1) {
- importDir = StringPool.SLASH.concat(
- importFileName.substring(0, slashPos + 1));
- }
- importContent = aggregateCss(dir + importDir, importContent);
- int importDepth = StringUtil.count(
- importFileName, StringPool.SLASH);
- // LEP-7540
- String relativePath = StringPool.BLANK;
- for (int i = 0; i < importDepth; i++) {
- relativePath += "../";
- }
- importContent = StringUtil.replace(
- importContent,
- new String[] {
- "url('" + relativePath,
- "url(\"" + relativePath,
- "url(" + relativePath
- },
- new String[] {
- "url('[$TEMP_RELATIVE_PATH$]",
- "url(\"[$TEMP_RELATIVE_PATH$]",
- "url([$TEMP_RELATIVE_PATH$]"
- });
- importContent = StringUtil.replace(
- importContent, "[$TEMP_RELATIVE_PATH$]", StringPool.BLANK);
- sb.append(importContent);
- pos = importY + _CSS_IMPORT_END.length();
- }
- }
- return sb.toString();
- }
其实这段代码非常简单,它就是找出页面上所有的css注释 /* */,然后把这些注释去除,然后找出页面上@import(url=)的这种外部css文件,递归的调用aggregateCSS直到他们不含有外部引入标记,然后把这些文件的内容(已经被去除了注释,换行符等)插入到引入它们的css文件中。
minifyJavaScript:
从第23行可以看出,当遇到文件扩展名是.js时,它就会调用minifyJavaScript方法来最小化这个js文件,并且设置content-type为text/javascript,最后把minify之后的文件存入cacheContentTypeFile。
我们来看下minifyJavaScript到底做了什么事情:
- protected String minifyJavaScript(File file) throws IOException {
- String content = FileUtil.read(file);
- return minifyJavaScript(content);
- }
它首先还是利用FileUtil来去除换行(见minifyCSS部分对这个方法的讲解),然后对于已经没有换行符的js文件继续调用minifyJavaScript():
- protected String minifyJavaScript(String content) {
- return MinifierUtil.minifyJavaScript(content);
- }
它又去调用MinifierUtil工具类方法来完成任务,最终执行任务的是MinifierUtil的_minifyJavaScript方法:
- private String _minifyJavaScript(String content) {
- UnsyncStringWriter unsyncStringWriter = new UnsyncStringWriter();
- try {
- JavaScriptCompressor javaScriptCompressor =
- new JavaScriptCompressor(
- new UnsyncStringReader(content),
- new JavaScriptErrorReporter());
- javaScriptCompressor.compress(
- unsyncStringWriter, _JS_LINE_BREAK, _JS_MUNGE, _JS_VERBOSE,
- _JS_PRESERVE_ALL_SEMICOLONS, _JS_DISABLE_OPTIMIZATIONS);
- }
- catch (Exception e) {
- _log.error("JavaScript Minifier failed for\n" + content);
- unsyncStringWriter.append(content);
- }
- return unsyncStringWriter.toString();
- }
它会先创建一个JavaScriptCompressor对象,然后用它来压缩没有换行符的js文件,采用的方式是和yahoo的yui-compressor一样的方式,算法很复杂,没必要一行行看了。
minifyJSP:
从34行可以看到,它会先判断是jsp扩展名,当然了, 它也不会对所有的jsp都生效,它生效的jsp文件都在liferay-web.xml中的这个filter的<init-param>中,具体的就是
barebone.jsp,everything.jsp等因为满足init-param的正则表达式的pattern,所以会通过这个过滤器。从第47-56行可以看出他会吧原来cache的所有被minify处理过的css或者js内容再minify一下然后写入String变量,然后第59-60行利用FileUtil进一步去除换行符,然后把cacheContentFile的内容以最终请求的MIME格式来复写一遍。
当我们在文章一开始的MinifierFilter的processFilter()方法中执行了所有的getMinifiedContent调用后:
- Object minifiedContent = getMinifiedContent(
- request, response, filterChain);
此时,这个Object minifiedContent的内容就不是null了。
现在我们来执行MinifierFilter的最后2个语句,它可以判断这个minifiedContent是个文件还是字符串,从而让其写入ServletResponse对象中并且返回给客户端,写入的方式是调用ServletResponseUtil工具类:
- else {
- if (minifiedContent instanceof File) {
- ServletResponseUtil.write(response, (File)minifiedContent);
- }
- else if (minifiedContent instanceof String) {
- ServletResponseUtil.write(response, (String)minifiedContent);
- }
- }
由此大功告成,我们所有的css,js资源文件都得到了最小化,然后整合成单个文件.
高级话题:barebone.jsp和everything.jsp
事实上,Liferay启用了2个配置,一个只引入最少最需要的js文件,最终组合为barebone.jsp,一个是引入所有的js文件,最终组合为everything.jsp,他们可以自由切换,切换代码在top_js.jspf中:
- <c:choose>
- <c:when test="<%= themeDisplay.isThemeJsFastLoad() %>">
- <c:choose>
- <c:when test="<%= themeDisplay.isThemeJsBarebone() %>">
- <script src="<%= HtmlUtil.escape(PortalUtil.getStaticResourceURL(request, themeDisplay.getPathJavaScript() + "/barebone.jsp", "minifierBundleId=javascript.barebone.files", javaScriptLastModified)) %>" type="text/javascript"></script>
- </c:when>
- <c:otherwise>
- <script src="<%= HtmlUtil.escape(PortalUtil.getStaticResourceURL(request, themeDisplay.getPathJavaScript() + "/everything.jsp", "minifierBundleId=javascript.everything.files", javaScriptLastModified)) %>" type="text/javascript"></script>
- </c:otherwise>
- </c:choose>
- </c:when>
- <c:otherwise>
这里可以看出来,切换主要去判断themeDisplay.isThemeJSBarebone,而这个配置在portal.properties中,比如我们服务器设置了javascript.barebone.enabled=true,则开启了barebone,则最后看情况可以有barebone.jsp可以有everything.jsp:
- #
- # Set this property to false to always load JavaScript files listed in the
- # property "javascript.everything.files". Set this to true to sometimes
- # load "javascript.barebone.files" and sometimes load
- # "javascript.everything.files".
- #
- # The default logic is coded in com.liferay.portal.events.ServicePreAction
- # in such a way that unauthenticated users get the list of barebone
- # JavaScript files whereas authenticated users get both the list of barebone
- # JavaScript files and the list of everything JavaScript files.
- #
- javascript.barebone.enabled=true
无论是barebone.jsp还是everything.jsp,他们的bundleId和读取目录都是预先是定好的:
- #
- # Input a list of comma delimited properties that are valid bundle ids for
- # the JavaScript minifier.
- #
- javascript.bundle.ids=\
- javascript.barebone.files,\
- javascript.everything.files
- #
- # Define a bundle directory for each property listed in
- # "javascript.bundle.ids".
- #
- javascript.bundle.dir[javascript.barebone.files]=/html/js
- javascript.bundle.dir[javascript.everything.files]=/html/js
- #
只不过barebone.jsp合并的js文件少,而everything.jsp文件合并全部的js文件:
barebone.jsp合并并且最小化哪些js文件呢?这也可以从portal.properties文件中找到答案:
- javascript.barebone.files=\
- \
- #
- # YUI core
- #
- \
- aui/yui/yui.js,\
- \
- #
- # YUI modules
- #
- \
- aui/anim-base/anim-base.js,\
- aui/anim-color/anim-color.js,\
- aui/anim-curve/anim-curve.js,\
- ...
而everything.jsp合并并且最小化哪些js文件呢?它是由javascript.barebone.files 包含的所有js文件,外加如下列表的不在barebone中的文件:
- #
- # Specify the list of everything files (everything else not already in the
- # list of barebone files).
- #
- javascript.everything.files=\
- \
- #
- # YUI modules
- #
- \
- aui/async-queue/async-queue.js,\
- aui/cookie/cookie.js,\
- aui/event-touch/event-touch.js,\
- aui/querystring-stringify/querystring-stringify.js,\
- \
- #
- # Alloy modules
- #
- \
- aui/aui-io/aui-io-plugin.js,\
- aui/aui-io/aui-io-request.js,\
- aui/aui-loading-mask/aui-loading-mask.js,\
- aui/aui-parse-content/aui-parse-content.js,\
- \
- #
- # Liferay modules
- #
- \
- liferay/address.js,\
- liferay/dockbar.js,\
- liferay/layout_configuration.js,\
- liferay/layout_exporter.js,\
- liferay/session.js,\
- \
- #
- # Deprecated JS
- #
- \
- liferay/deprecated.js
这样一分析下来,整个Liferay框架的静态资源加载文件就非常清晰了。