Ответ 1
Этот ответ не распространяется на минимизацию и сжатие. Минимизировать отдельные ресурсы CSS/JS лучше делегировать для создания скриптов, таких как задача YUI Compressor Ant. Вручную делать это по каждому запросу слишком дорого. Сжатие (я предполагаю, что вы имеете в виду GZIP?) Лучше всего делегировать в контейнер сервлетов, который вы используете. Вручную делать это сложно. Например, на 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
и 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()
и геттерами в зависимости от одного из свойств, которые должны быть установлены)
И вот как выглядит 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 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
страница витрины и документация по API для получения дополнительной информации. деталь.