Этот ответ не охватывает минимизацию и сжатие. Сокращение отдельных ресурсов CSS / JS лучше делегировать для создания сценариев, таких как YUI Compressor Ant task . Делать это вручную по каждому запросу слишком дорого. Сжатие (я полагаю, вы имеете в виду 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
s объединенных ресурсов в одном 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'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
страница витрины и документация API для получения более подробной информации.