Управление статическими ресурсами JSF2 - комбинированное, сжатое - PullRequest
16 голосов
/ 14 октября 2011

Кто-нибудь знает метод динамического объединения / минимизации всех ресурсов h: outputStylesheet, а затем объединения / минимизации всех ресурсов h: outputScript на этапе рендеринга? Объединенный / минимизированный ресурс, вероятно, должен был бы быть кэширован с ключом, основанным на объединенной строке ресурса или чем-то, чтобы избежать чрезмерной обработки.

Если этой функции не существует, я бы хотел поработать над ней. У кого-нибудь есть идеи, как лучше всего реализовать что-то подобное? Полагаю, что сервлетный фильтр будет работать, но фильтр должен будет выполнять больше работы, чем необходимо - в основном, анализируя весь вывод и заменяя совпадения. Реализация чего-либо на этапе рендеринга выглядит так, как будто это будет работать лучше, поскольку все статические ресурсы доступны без разбора всего вывода.

Спасибо за любые предложения!

Редактировать: Чтобы показать, что я не ленивый и действительно поработаю над этим с некоторыми рекомендациями, вот заглушка, которая захватывает имя / библиотеку ресурсов скрипта, а затем удаляет их из представления , Как вы видите, у меня есть несколько вопросов о том, что делать дальше ... я должен сделать 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);
            }

        }
    }

}

Ответы [ 4 ]

12 голосов
/ 28 октября 2011

Этот ответ не охватывает минимизацию и сжатие. Сокращение отдельных ресурсов 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 для получения более подробной информации.

3 голосов
/ 15 сентября 2013

Предоставлены Omnifaces CombinedResourceHandler - отличная утилита, но я также люблю поделиться этим превосходным плагином maven: - resources-optimizer-maven-plugin, который можно использовать для минимизации / сжатия js Файлы / css и / или объединяют их в меньшее количество ресурсов во время сборки, а не динамически во время выполнения, что делает его более производительным решением, я считаю.

Также посмотрите на эту превосходную библиотеку: - webutilities

3 голосов
/ 27 октября 2011

Возможно, вы захотите оценить JAWR , прежде чем внедрять собственное решение.Я использовал его в нескольких проектах, и это имело большой успех.Он использовался в проектах JSF 1.2, но я думаю, что будет легко расширить его для работы с JSF 2.0.Просто попробуйте.

0 голосов
/ 09 июля 2013

У меня есть другое решение для JSF 2. Может также быть с JSF 1, но я не знаю JSF 1, поэтому не могу сказать. Идея работает в основном с компонентами из h: head и работает также с таблицами стилей. Результат всегда один файл JavaScript (или таблицы стилей) для страницы! Мне сложно описать, но я стараюсь.

Я перегружаю стандартный JSF ScriptRenderer (или StylesheetRenderer) и настраиваю рендер для компонента h: outputScript в файлеface-config.xml. Новый рендерер больше не будет писать скрипт-тег, но будет собирать все ресурсы в списке. Таким образом, первый ресурс, который будет отображен, будет первым элементом в списке, следующий следует и так далее. После рендеринга последнего компонента h: outputScript вам нужно сделать 1 скрипт-тег для файла JavaScript на этой странице. Я делаю это, перегружая рендер h: head.

Теперь приходит идея: Я зарегистрировал фильтр! Фильтр будет искать этот 1 запрос script-Tag. Когда приходит этот запрос, я получу список ресурсов для этой страницы. Теперь я могу заполнить ответ из списка Ресурсы. Порядок будет правильным, потому что рендеринг JSF разместил ресурсы в правильном порядке. в список. После заполнения ответа список следует очистить. Также вы можете сделать больше оптимизации, потому что у вас есть код в фильтре ....

У меня есть код, который работает превосходно. Мой код также может обрабатывать кеширование в браузере и динамический рендеринг скриптов. Если кому-то интересно, могу поделиться кодом.

...