JSF2 静态资源管理——组合、压缩

标签 jsf static jsf-2 minify

有谁知道动态组合/缩小所有 h:outputStylesheet 资源然后在渲染阶段组合/缩小所有 h:outputScript 资源的方法?合并/缩小的资源可能需要使用基于组合资源字符串或其他内容的键进行缓存,以避免过度处理。

如果此功能不存在,我想处理它。有没有人对实现这样的事情的最佳方式有想法。我想 Servlet 过滤器可以工作,但过滤器必须做比必要更多的工作——基本上检查整个渲染输出并替换匹配项。在渲染阶段实现某些东西似乎会更好,因为所有静态资源都可用,而无需解析整个输出。

感谢您的任何建议!

编辑:为了表明我并不懒惰并且会在一些指导下真正解决这个问题,这里有一个 stub ,它捕获脚本资源名称/库,然后将它们从 View 中删除。如您所见,我对下一步要做什么有一些疑问……我应该发出 http 请求并获取要合并的资源,然后合并它们并将它们保存到资源缓存中吗?

package com.davemaple.jsf.listener;

import java.util.ArrayList;
import java.util.List;

import javax.faces.component.UIComponent;
import javax.faces.component.UIOutput;
import javax.faces.component.UIViewRoot;
import javax.faces.context.FacesContext;
import javax.faces.event.AbortProcessingException;
import javax.faces.event.PhaseEvent;
import javax.faces.event.PhaseId;
import javax.faces.event.PhaseListener;
import javax.faces.event.PreRenderViewEvent;
import javax.faces.event.SystemEvent;
import javax.faces.event.SystemEventListener;

import org.apache.log4j.Logger;

/**
 * A Listener that combines CSS/Javascript Resources
 * 
 * @author David Maple<d@davemaple.com>
 *
 */
public class ResourceComboListener implements PhaseListener, SystemEventListener {

    private static final long serialVersionUID = -8430945481069344353L;
    private static final Logger LOGGER = Logger.getLogger(ResourceComboListener.class);

    @Override
    public PhaseId getPhaseId() {
        return PhaseId.RESTORE_VIEW;
    }

    /*
     * (non-Javadoc)
     * @see javax.faces.event.PhaseListener#beforePhase(javax.faces.event.PhaseEvent)
     */
    public void afterPhase(PhaseEvent event) {
        FacesContext.getCurrentInstance().getViewRoot().subscribeToViewEvent(PreRenderViewEvent.class, this);
    }

    /*
     * (non-Javadoc)
     * @see javax.faces.event.PhaseListener#afterPhase(javax.faces.event.PhaseEvent)
     */
    public void beforePhase(PhaseEvent event) {
        //nothing here
    }

    /*
     * (non-Javadoc)
     * @see javax.faces.event.SystemEventListener#isListenerForSource(java.lang.Object)
     */
    public boolean isListenerForSource(Object source) {
        return (source instanceof UIViewRoot);
    }

    /*
     * (non-Javadoc)
     * @see javax.faces.event.SystemEventListener#processEvent(javax.faces.event.SystemEvent)
     */
    public void processEvent(SystemEvent event) throws AbortProcessingException {
        FacesContext context = FacesContext.getCurrentInstance();
        UIViewRoot viewRoot = context.getViewRoot();
        List<UIComponent> scriptsToRemove = new ArrayList<UIComponent>();

        if (!context.isPostback()) {

            for (UIComponent component : viewRoot.getComponentResources(context, "head")) {
                if (component.getClass().equals(UIOutput.class)) {
                    UIOutput uiOutput = (UIOutput) component;

                    if (uiOutput.getRendererType().equals("javax.faces.resource.Script")) {
                        String library = uiOutput.getAttributes().get("library").toString();
                        String name = uiOutput.getAttributes().get("name").toString();

                        // make https requests to get the resources?
                        // combine then and save to resource cache?
                        // insert new UIOutput script?

                        scriptsToRemove.add(component);
                    }


                }
            }

            for (UIComponent component : scriptsToRemove) {
                viewRoot.getComponentResources(context, "head").remove(component);
            }

        }
    }

}

最佳答案

这个答案不包括缩小和压缩。最好将单个 CSS/JS 资源的最小化委派来构建像 YUI Compressor Ant task 这样的脚本.在每个请求上手动执行它太昂贵了。压缩(我假设您的意思是 GZIP?)最好委托(delegate)给您正在使用的 servlet 容器。手动执行它过于复杂。例如,在 Tomcat 上,只需添加 compression="on" <Connector> 的属性/conf/server.xml 中的元素.

SystemEventListener 已经是一个很好的第一步(除了一些 PhaseListener 不必要的)。接下来,您需要实现自定义 ResourceHandler Resource .那部分并不完全是微不足道的。如果您想独立于 JSF 实现,则需要进行大量的改造。

首先,在您的 SystemEventListener ,您想创建新的 UIOutput 表示组合资源的组件,以便您可以使用 UIViewRoot#addComponentResource() 添加它.你需要设置它的library归因于您的自定义资源处理程序可以理解的独特事物。您需要根据资源组合(可能是 MD5 哈希?)将组合资源沿唯一名称存储在应用程序范围的变量中,然后将此键设置为 name组件的属性。存储为应用程序范围的变量对服务器和客户端都有缓存优势。

像这样的东西:

String combinedResourceName = CombinedResourceInfo.createAndPutInCacheIfAbsent(resourceNames);
UIOutput component = new UIOutput();
component.setRendererType(rendererType);
component.getAttributes().put(ATTRIBUTE_RESOURCE_LIBRARY, CombinedResourceHandler.RESOURCE_LIBRARY);
component.getAttributes().put(ATTRIBUTE_RESOURCE_NAME, combinedResourceName + extension);
context.getViewRoot().addComponentResource(context, component, TARGET_HEAD);

然后,在您的自定义 ResourceHandler 实现,您需要实现 createResource() 方法相应地创建自定义 Resource 每当库匹配所需值时执行:
@Override
public Resource createResource(String resourceName, String libraryName) {
    if (RESOURCE_LIBRARY.equals(libraryName)) {
        return new CombinedResource(resourceName);
    } else {
        return super.createResource(resourceName, libraryName);
    }
}

自定义的构造函数 Resource 实现应该根据名称获取组合资源信息:
public CombinedResource(String name) {
    setResourceName(name);
    setLibraryName(CombinedResourceHandler.RESOURCE_LIBRARY);
    setContentType(FacesContext.getCurrentInstance().getExternalContext().getMimeType(name));
    this.info = CombinedResourceInfo.getFromCache(name.split("\\.", 2)[0]);
}

这个风俗 Resource 实现必须提供适当的 getRequestPath() 方法返回一个 URI,然后将其包含在呈现的 <script> 中或 <link>元素:
@Override
public String getRequestPath() {
    FacesContext context = FacesContext.getCurrentInstance();
    String path = ResourceHandler.RESOURCE_IDENTIFIER + "/" + getResourceName();
    String mapping = getFacesMapping();
    path = isPrefixMapping(mapping) ? (mapping + path) : (path + mapping);
    return context.getExternalContext().getRequestContextPath()
        + path + "?ln=" + CombinedResourceHandler.RESOURCE_LIBRARY;
}

现在,HTML 渲染部分应该没问题了。它看起来像这样:

<link type="text/css" rel="stylesheet" href="/playground/javax.faces.resource/dd08b105bf94e3a2b6dbbdd3ac7fc3f5.css.xhtml?ln=combined.resource" />
<script type="text/javascript" src="/playground/javax.faces.resource/2886165007ccd8fb65771b75d865f720.js.xhtml?ln=combined.resource"></script>

接下来,您必须拦截浏览器发出的组合资源请求。这是最难的部分。首先,在您的自定义 ResourceHandler 实现,您需要实现 handleResourceRequest() 相应的方法:
@Override
public void handleResourceRequest(FacesContext context) throws IOException {
    if (RESOURCE_LIBRARY.equals(context.getExternalContext().getRequestParameterMap().get("ln"))) {
        streamResource(context, new CombinedResource(getCombinedResourceName(context)));
    } else {
        super.handleResourceRequest(context);
    }
}

然后你必须做大量的工作来实现自定义 Resource 的其他方法。相应地实现,例如 getResponseHeaders() 应该返回正确的缓存头, getInputStream() 应该返回 InputStream单个 InputStream 中的组合资源的 s和 userAgentNeedsUpdate() 它应该正确响应缓存相关的请求。
@Override
public Map<String, String> getResponseHeaders() {
    Map<String, String> responseHeaders = new HashMap<String, String>(3);
    SimpleDateFormat sdf = new SimpleDateFormat(PATTERN_RFC1123_DATE, Locale.US);
    sdf.setTimeZone(TIMEZONE_GMT);
    responseHeaders.put(HEADER_LAST_MODIFIED, sdf.format(new Date(info.getLastModified())));
    responseHeaders.put(HEADER_EXPIRES, sdf.format(new Date(System.currentTimeMillis() + info.getMaxAge())));
    responseHeaders.put(HEADER_ETAG, String.format(FORMAT_ETAG, info.getContentLength(), info.getLastModified()));
    return responseHeaders;
}

@Override
public InputStream getInputStream() throws IOException {
    return new CombinedResourceInputStream(info.getResources());
}

@Override
public boolean userAgentNeedsUpdate(FacesContext context) {
    String ifModifiedSince = context.getExternalContext().getRequestHeaderMap().get(HEADER_IF_MODIFIED_SINCE);

    if (ifModifiedSince != null) {
        SimpleDateFormat sdf = new SimpleDateFormat(PATTERN_RFC1123_DATE, Locale.US);

        try {
            info.reload();
            return info.getLastModified() > sdf.parse(ifModifiedSince).getTime();
        } catch (ParseException ignore) {
            return true;
        }
    }

    return true;
}

我在这里有一个完整的工作概念证明,但是作为 SO 答案发布的代码太多了。以上只是部分帮助您朝着正确的方向发展。我假设缺少的方法/变量/常量声明是不言自明的,足以编写您自己的声明,否则请告诉我。

更新:根据评论,您可以在 CombinedResourceInfo 中收集资源:
private synchronized void loadResources(boolean forceReload) {
    if (!forceReload && resources != null) {
        return;
    }

    FacesContext context = FacesContext.getCurrentInstance();
    ResourceHandler handler = context.getApplication().getResourceHandler();
    resources = new LinkedHashSet<Resource>();
    contentLength = 0;
    lastModified = 0;

    for (Entry<String, Set<String>> entry : resourceNames.entrySet()) {
        String libraryName = entry.getKey();

        for (String resourceName : entry.getValue()) {
            Resource resource = handler.createResource(resourceName, libraryName);
            resources.add(resource);

            try {
                URLConnection connection = resource.getURL().openConnection();
                contentLength += connection.getContentLength();
                long lastModified = connection.getLastModified();

                if (lastModified > this.lastModified) {
                    this.lastModified = lastModified;
                }
            } catch (IOException ignore) {
                // Can't and shouldn't handle it here anyway.
            }
        }
    }
}

(上述方法由 reload() 方法和 getter 调用,具体取决于要设置的属性之一)

这就是 CombinedResourceInputStream看起来像:
final class CombinedResourceInputStream extends InputStream {

    private List<InputStream> streams;
    private Iterator<InputStream> streamIterator;
    private InputStream currentStream;

    public CombinedResourceInputStream(Set<Resource> resources) throws IOException {
        streams = new ArrayList<InputStream>();

        for (Resource resource : resources) {
            streams.add(resource.getInputStream());
        }

        streamIterator = streams.iterator();
        streamIterator.hasNext(); // We assume it to be always true; CombinedResourceInfo won't be created anyway if it's empty.
        currentStream = streamIterator.next();
    }

    @Override
    public int read() throws IOException {
        int read = -1;

        while ((read = currentStream.read()) == -1) {
            if (streamIterator.hasNext()) {
                currentStream = streamIterator.next();
            } else {
                break;
            }
        }

        return read;
    }

    @Override
    public void close() throws IOException {
        IOException caught = null;

        for (InputStream stream : streams) {
            try {
                stream.close();
            } catch (IOException e) {
                if (caught == null) {
                    caught = e; // Don't throw it yet. We have to continue closing all other streams.
                }
            }
        }

        if (caught != null) {
            throw caught;
        }
    }

}

更新 2 :OmniFaces 中提供了一个具体且可重复使用的解决方案。另见 CombinedResourceHandler showcase pageAPI documentation了解更多详情。

关于JSF2 静态资源管理——组合、压缩,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/7768109/

相关文章:

c++ - 子类不是父类的静态成员

java - 如何在 java webapp 中的服务器上保存和检索图像

jsf - Primefaces 数据表滚动未按预期工作

java - 将用户名和密码从 JSF 或 JSP 传递到 Oracle Reports

c++ - 对 `Static Class Member variable inside Static member function' 的 undefined reference

java - 使用带有嵌套静态类的类的好习惯是,然后使用更多静态方法扩展包私有(private)抽象类以保持组织性?

java - JSF 2.0 动态属性,无需创建新组件

jsf - 使用过滤器类的重定向过多

java - 在 tomcat + jsf 中的 https 之后重定向到 http

security - 使用容器管理的身份验证时如何在 JSF 中获取登录用户名