是否有人知道一种方法来动态组合/缩小所有h:outputStylesheet资源,然后在渲染阶段合并/缩小所有h:outputScript资源?可能需要使用基于组合资源String的键来缓存已压缩/缩小的资源,以避免过度处理。
如果此功能不存在,我想继续处理。有没有人有关于实现这样的事情的最佳方法的想法。我认为Servlet过滤器可以正常工作,但过滤器必须做更多的工作 - 基本上检查整个渲染输出并替换匹配。在呈现阶段实现某些东西似乎会更好地工作,因为所有静态资源都可用而无需解析整个输出。
感谢您的任何建议!
编辑: 为了表明我不是懒惰并且会在一些指导下真正开展这项工作,这里有一个存根捕获脚本资源名称/库,然后将其从视图。正如您所看到的,我对下一步该做什么有一些疑问......我应该发出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);
}
}
}
}
答案 0 :(得分:12)
这个答案不包括缩小和压缩。简化单个CSS / JS资源最好委托构建像YUI Compressor Ant task这样的脚本。在每个请求上手动执行它太昂贵了。压缩(我假设你的意思是GZIP?)最好委托给你正在使用的servlet容器。手动执行它是过于复杂的。例如,在Tomcat上添加compression="on"
属性到<Connector>
中的/conf/server.xml
元素。
SystemEventListener
已经是一个很好的第一步(除了一些PhaseListener
不必要的)。接下来,您需要实施自定义ResourceHandler
和Resource
。那部分并非完全无足轻重。如果你想独立于JSF实现,你需要重新发明很多。
首先,在您的SystemEventListener
中,您希望创建代表组合资源的新UIOutput
组件,以便您可以使用UIViewRoot#addComponentResource()
添加它。您需要将其library
属性设置为 unique ,这是您的自定义资源处理程序所理解的。您需要根据资源的组合(可能是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,然后该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
和userAgentNeedsUpdate()
中合并资源的InputStream
个,应该对缓存相关请求做出正确响应。
@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 page和API documentation。
答案 1 :(得分:3)
您可能希望在实施自己的解决方案之前评估JAWR。我已经在几个项目中使用它,这是一个巨大的成功。它在JSF 1.2项目中使用,但我认为将它扩展到使用JSF 2.0很容易。试试吧。
答案 2 :(得分:3)
Omnifaces提供CombinedResourceHandler
是一个很好的实用工具,但我也很想分享这个优秀的maven插件: - resources-optimizer-maven-plugin
可以用来缩小/压缩js / css文件&amp; /或聚合在构建时间内将它们分成更少的资源。在运行期间不是动态的,这使它成为一个更高效的解决方案,我相信。
还要看看这个优秀的图书馆: - webutilities
答案 3 :(得分:0)
我有另一个JSF 2的解决方案。可能还会使用JSF 1,但我不知道JSF 1,所以我不能说。 Idea主要使用h:head的组件,也适用于样式表。结果 始终是页面的一个JavaScript(或样式表)文件!我很难描述,但我尝试。
我重载标准JSF ScriptRenderer(或StylesheetRenderer)并配置渲染器 对于faces-config.xml中的h:outputScript组件。 新的渲染器现在不再编写脚本标签,但它将收集所有资源 在列表中。因此,要呈现的第一个资源将是列表中的第一个项目,接下来是 等等。在最后一次h:渲染输出组件之后,您必须渲染1个脚本标记 对于此页面上的JavaScript文件。我通过重载h:head渲染器来实现这一点。
现在出现了这个想法: 我注册了一个过滤器!过滤器将查找此1脚本标记请求。当这个请求到来时, 我将获得此页面的资源列表。现在我可以填写清单中的回复 资源。顺序是正确的,因为JSF渲染以正确的顺序放置资源 进入清单。响应填满后,应清除列表。你也可以做更多 优化,因为你有过滤器中的代码....
我的代码非常棒。我的代码也可以处理浏览器缓存和动态脚本呈现。 如果有人感兴趣,我可以分享代码。