为了账号安全,请及时绑定邮箱和手机立即绑定

Liferay MinifierFilter的研究

标签:
JQuery

 大家都知道,在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 class="lazyload" src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsQAAA7EAZUrDhsAAAANSURBVBhXYzh8+PB/AAffA0nNPuCLAAAAAElFTkSuQmCC" data-original="<%= HtmlUtil.escape(PortalUtil.getStaticResourceURL(request, themeDisplay.getPathJavaScript() + "/barebone.jsp", "minifierBundleId=javascript.barebone.files", javaScriptLastModified)) %>" type="text/javascript"></script>             </c:when>             <c:otherwise>                 <script class="lazyload" src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsQAAA7EAZUrDhsAAAANSURBVBhXYzh8+PB/AAffA0nNPuCLAAAAAElFTkSuQmCC" data-original="<%= 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框架的静态资源加载文件就非常清晰了。

点击查看更多内容
1人点赞

若觉得本文不错,就分享一下吧!

评论

作者其他优质文章

正在加载中
感谢您的支持,我会继续努力的~
扫码打赏,你说多少就多少
赞赏金额会直接到老师账户
支付方式
打开微信扫一扫,即可进行扫码打赏哦
今天注册有机会得

100积分直接送

付费专栏免费学

大额优惠券免费领

立即参与 放弃机会
意见反馈 帮助中心 APP下载
官方微信

举报

0/150
提交
取消