Liferay Dynamic CSS Filter方法的研究 - 总体过程

背景知识:

最近项目组遇到一个问题就是改了一个new theme之后导致某些css文件不起作用了,这也激起了我的好奇心,让我有机会去研究下Liferay Dynamic CSS Filter的原理。


引入

这个Filter 和一般的Filter一样,会配置在portal-web.xml中,并且声明了对于.css文件和.jsp资源文件请求时候会触发:


然后执行它的processFilter 方法:

protected void processFilter(
            HttpServletRequest request, HttpServletResponse response,
            FilterChain filterChain)
        throws Exception {
        Object parsedContent = getDynamicContent(
            request, response, filterChain);
        if (parsedContent == null) {
            processFilter(
                DynamicCSSFilter.class, request, response, filterChain);
        }
        else {
            if (parsedContent instanceof File) {
                ServletResponseUtil.write(response, (File)parsedContent);
            }
            else if (parsedContent instanceof String) {
                ServletResponseUtil.write(response, (String)parsedContent);
            }
        }
    }



调试场景:

比如当我们刚加载 liferay首页,因为上面有许多css资源文件,所以会自动触发这个调用,走入processFilter方法,而它会调用getDynamicContent()方法来获取jRuby解析Sass后的变成的普通css文件。这方法是我们这文章研究的重点。


getDynamicContent()的代码如下:

protected Object getDynamicContent(
            HttpServletRequest request, HttpServletResponse response,
            FilterChain filterChain)
        throws Exception {
        String requestURI = request.getRequestURI();
        String requestPath = requestURI;
        String contextPath = request.getContextPath();
        if (!contextPath.equals(StringPool.SLASH)) {
            requestPath = requestPath.substring(contextPath.length());
        }
        String realPath = ServletContextUtil.getRealPath(
            _servletContext, requestPath);
        if (realPath == null) {
            return null;
        }
        realPath = StringUtil.replace(
            realPath, CharPool.BACK_SLASH, CharPool.SLASH);
        File file = new File(realPath);
        String cacheCommonFileName = getCacheFileName(request);
        File cacheContentTypeFile = new File(
            cacheCommonFileName + "_E_CONTENT_TYPE");
        File cacheDataFile = new File(cacheCommonFileName + "_E_DATA");
        if ((cacheDataFile.exists()) &&
            (cacheDataFile.lastModified() >= file.lastModified())) {
            if (cacheContentTypeFile.exists()) {
                String contentType = FileUtil.read(cacheContentTypeFile);
                response.setContentType(contentType);
            }
            return cacheDataFile;
        }
        String dynamicContent = null;
        String content = null;
        try {
            if (realPath.endsWith(_CSS_EXTENSION) && file.exists()) {
                if (_log.isInfoEnabled()) {
                    _log.info("Parsing SASS on CSS " + file);
                }
                content = FileUtil.read(file);
                dynamicContent = DynamicCSSUtil.parseSass(
                    request, realPath, content);
                response.setContentType(ContentTypes.TEXT_CSS);
                FileUtil.write(cacheContentTypeFile, ContentTypes.TEXT_CSS);
            }
            else if (realPath.endsWith(_JSP_EXTENSION) || !file.exists()) {
                if (_log.isInfoEnabled()) {
                    _log.info("Parsing SASS on JSP or servlet " + realPath);
                }
                StringServletResponse stringResponse =
                    new StringServletResponse(response);
                processFilter(
                    DynamicCSSFilter.class, request, stringResponse,
                    filterChain);
                CacheResponseUtil.setHeaders(
                    response, stringResponse.getHeaders());
                response.setContentType(stringResponse.getContentType());
                content = stringResponse.getString();
                dynamicContent = DynamicCSSUtil.parseSass(
                    request, realPath, content);
                FileUtil.write(
                    cacheContentTypeFile, stringResponse.getContentType());
            }
            else {
                return null;
            }
        }
        catch (Exception e) {
            _log.error("Unable to parse SASS on CSS " + realPath, e);
            if (_log.isDebugEnabled()) {
                _log.debug(content);
            }
            response.setHeader(
                HttpHeaders.CACHE_CONTROL,
                HttpHeaders.CACHE_CONTROL_NO_CACHE_VALUE);
        }
        if (dynamicContent != null) {
            FileUtil.write(cacheDataFile, dynamicContent);
        }
        else {
            dynamicContent = content;
        }
        return dynamicContent;
    }



我们附上调试信息:


从上述调试信息一目了然,主要是第5行获取当前请求的URI,从调试信息看,它的内容是:

html/portlet/login/css/main.css , 这个也正符合我们的猜想,因为当前请求的资源文件main.css符合CSS扩展名的模式,所以才被这个Dynamic CSS Filter所过滤到并且进入这个断点。


第16行获取这个资源文件的真实路径realPath(疑问1: 如何获取这个真实的path的? 答案在以后讨论中给出) ,这里给出的路径是

/app/Liferay/RI/liferay-portal-6.1.0-ce-ga1/tomcat-7.0.23/webapps/ROOT/html/portlet/login/css/main.css

,这也正符合我们当初的设想,因为这个main.css我们的确是一年前把它手动复制到了该目录下。


然后第19行计算出这个文件对应的缓存base文件名,因为一个缓存文件总是由2部分组成,一个是内容类型文件,一个是数据文件,他们的各自名字都是由base名字加上指定后缀拼接而成。内容类型文件的名字是<cacheCommonFileName>_E_CONTENT_TYPE,而缓存数据文件的名字是<cacheCommonFileName>_E_DATA. (疑问2:如何计算得到这个缓存base文件名?答案也在后续讨论中给出) ,所以我们通过计算得到的缓存base文件名为:

/app/Liferay/RI/liferay-portal-6.1.0-ce-ga1/tomcat-7.0.23/temp/liferay/css/portal/623927847558055413


下面既然得到了缓存base文件名,并且依照后缀定则拼接了相应的内容类型文件名和数据文件名,那么下面的工作就是第20行和第21行在相应位置创建相应的File对象了。因为new File()按照我们对于java的语义,就是如果这个文件不存在,那么则创建新文件,如果存在,只File对象指向已知文件。

我们到服务器目录下看到了这个2个文件:


当缓存内容类型文件和缓存内容文件都固定下来后,下面就考虑到更新或者填入内容到这些文件了。

首先,从第35行开始,还是从原始的带有Sass的css文件入手:

if (realPath.endsWith(_CSS_EXTENSION) && file.exists()) {
                if (_log.isInfoEnabled()) {
                    _log.info("Parsing SASS on CSS " + file);
                }
                content = FileUtil.read(file);
                dynamicContent = DynamicCSSUtil.parseSass(
                    request, realPath, content);
                response.setContentType(ContentTypes.TEXT_CSS);
                FileUtil.write(cacheContentTypeFile, ContentTypes.TEXT_CSS);
            }

它先判断原始文件是不是CSS扩展名的文件,如果是,那么就读取这个原始的css文件到一个字符串变量content中,见以下的调试信息:


然后调用DynamicCSSUtil的parseSass()方法吧这个带Sass的css文件解析成一个不带Sass的普通css文件,并且结果存放在dynamicContent变量中,比如上述content被解析后存放到的dynamicContent的值如下:

读者很容易看出这个新的样式文件是和原来不一样了,不仅仅是排版格式还有语法。


最后,把相应的内容写入到刚才最早的生成的内容类型文件和数据文件中。

源代码的第64行在内容类型文件(<cacheCommonFileName>_E_CONTENT_TYPE)中写入内容为 text/css

FileUtil.write(cacheContentTypeFile, ContentTypes.TEXT_CSS);


而第82行在数据文件中(<cacheCommonFileName>_E_DATA)写入刚才生成Sass解析后生成的普通css文件内容

FileUtil.write(cacheContentTypeFile, stringResponse.getContentType()

(疑问3:这个写入过程细节是这样的呢?比如文件为空和文件中已经有内容各是如何处理的? 这个答案也在以后讨论中给出)


最后在第88行中返回的动态生成的普通css文件字符串。




现在我们返回到processFilter方法中,既然已经得到了Sass解析后生成的普通css字符串,所以最后就是把这个字符串返回到客户端,所以在processFilter()方法的行末:

ServletResponseUtil.write(response, (String)parsedContent);

这样我们访问页面时候就可以正确的看到和使用这里的样式了。



总结:

从非常宏观的角度,我们至少有以下几点收获:

(1)DynamicCssFilter的调用时机是在站点请求响应的资源文件的时候触发的。

(2)访问资源文件时,它会从原始含有Sass语法的css文件中获取原始内容,然后用jRuby引擎进行解析从而获得新的解析后的普通css文件。

(3)解析后的css文件总会最终被写入到缓存内容文件中,这个缓存数据文件的后缀总是_E_DATA,并且它总是对应一个内容类型文件,这个内容类型文件的格式总是_E_CONTENT_TYPE.

(4)解析后的文件会被服务器写到最终输出流中,从而你在浏览器中可以看到并且使用这个被解析后的普通css文件。


我们还留着几个疑点,会在接下来的文章中得到解决。

你可能感兴趣的:(css,过滤器,sass)