Пример Fast Container

 

Автор: Ренат Ахмеров

Этот пример демонстрирует работу FastContainer, описанного в нашей статье “GWT: улучшаем производительность”. Цель примера – рассмотреть свойства setInnerHtml в деталях, и дать временную оценку результата (в миллисекундах). Помимо этого, я остановлюсь на некоторых важных аспектах реализации FastContainer. Работающий пример можно найти на http://www.stravati.com/examples/fastcontainer и скачать с http://www.stravati.com/examples/sources/fast-container-demo-src.zip.

Страница примера состоит из 2 частей – левой и правой. Здесь отображаются тестовые компоненты. Для отрисовки компонентов в левой части мы использовали FlexTable в качестве контейнера для мелких элементов, что является наиболее часто используемым способом при разработке на GWT. Этот тип компонента был позаимствован из нашего прошлого проекта, где он отвечал за несколько критических задач. В простейшем случае он состоит из чекбокса (завершено / не завершено), заголовка (описания) задачи, и двух дат – начального и конечного сроков. Всё это можно увидеть на рисунке ниже.

GWT component

В правой части каждый компонент реализован с помощью FastContainer. Вверху страницы есть специальный чекбокс, включающий или отключающий использование управляющих элементов в компоненте. Давайте разберемся в коде поглубже.

Стандартный компонент, как уже было сказано выше, реализованный с использованием FlexTable. Его код прост и понятен. Следующий листинг описывает 2 метода создания структур для компонента:

         private FlexTable getFrame() {  		if (frame == null) {  			frame = new FlexTable();  			frame.setStyleName("item");  			frame.setCellSpacing(0);  			frame.setWidget(1, 1, getLayout());  			frame.setWidget(2, 2, new Label(""));    			FlexCellFormatter formatter = frame.getFlexCellFormatter();    			formatter.setStyleName(0, 0, "item-leftTop");  			formatter.setStyleName(0, 1, "item-top");  			formatter.setStyleName(0, 2, "item-rightTop");    			formatter.setStyleName(1, 0, "item-leftMiddle");  			formatter.setStyleName(1, 1, "item-contentCell");  			formatter.setStyleName(1, 2, "item-rightMiddle");    			formatter.setStyleName(2, 0, "item-leftBottom");  			formatter.setStyleName(2, 1, "item-bottom");  			formatter.setStyleName(2, 2, "item-rightBottom");  		}  		return frame;  	}    	private FlexTable getLayout() {  		if (layout == null) {  			layout = new FlexTable();  			layout.setWidth("100%");  			layout.setCellSpacing(0);    			layout.setWidget(0, 2, getKickOffLabel());  			layout.setWidget(1, 2, getDoByLabel());  		}  		return layout;  	}

Скругленные углы здесь создаются как фоновые изображения для ячеек таблицы в соответствующих классах CSS. Полные таблицы стилей можно найти в исходном коде. А здесь приведён один из методов управления компонентом:

	public void setTitleWidget(Widget widget) {  		getLayout().setWidget(0, 1, widget);  	}

Этот метод, по сути, описывает, где внутри контейнера будет располагаться заглавный виджет. Также существуют методы и для других элементов (чекбокса и дат). В нашем простейшем приложении такие понятные компоненты выведены наружу и управляются entry-point классом FCEntryPoint следующим способом:

	StandardComponent aComponent = new StandardComponent();  	Label title = new Label("Some title here");  	title.setStyleName("item-title-label");  	aComponent.setTitleWidget(title);  	aComponent.setCheckboxWidget(new CheckBox());  	aComponent.setKickOffWidget(new Label("Today"));  	aComponent.setDoByWidget(new Label("Tomorrow"));

Естественно, для управления компонентами (обработчики событий, стили, управление фокусом) будет задействован более сложный код, и скорее всего он будет инкапсулирован внутри класса, обеспечивающего интерфейс для работы с компонентами на высоком уровне (напр. значение Date напротив Label, значение boolean напротив Checkbox,и т.д). Но в данном случае мы преследуем цель создать простое приложение с упором на FastContainer, не перегружая код добавочными классами.
Существует очень много занимательных моментов, связанных с компонентом на основе FastContainer, играющем ключевую роль в жизненном цикле компонента. В этом классе не так уж много кода, поэтому приведу его полностью:

public class FastContainer extends Widget {    	private Context context;    	public FastContainer(Pattern pattern) {  		this.context = new Context();    		Element creatorElement = DOM.createDiv();  		DOM.appendChild(RootPanel.getBodyElement(), creatorElement);    		DOM.setInnerHTML(creatorElement, pattern.generatePattern(context));    		Element elem = DOM.getFirstChild(creatorElement);  		setElement(elem);  	}    	public void setWidget(String fieldName, Widget widget) {  		Parameter param = context.getParameter(fieldName);  		if (param == null) {  			throw new RuntimeException("There is no such parameter in context!");  		}  		Element element = DOM.getElementById(param.getId());  		if (element != null) {  			DOM.appendChild(element, widget.getElement());  			DOM.setEventListener(widget.getElement(), widget);  		}  	}  }

Для обеспечения возможности работы с обычным HTML в компоненте FastContainer объединяется с Pattern. Последний отвечает за генерацию исходного HTML-кода и создание набора шаблонных параметров внутри Context. Если присмотреться к коду повнимательнее, можно заметить весьма сложно организованный алгоритм, инициализирующий DOM-элемент компонента. Если делать это, просто направляя внутренний HTML внутрь элемента, а потом вызывая DOM.getElementById(param.getId()) , оно отказывается работать по невыясненной причине. Поэтому нужно использовать обходной метод для того, чтобы обеспечить возможность прямого присоединения исходного HTML-кода. Также лучше закомментировать последнюю строку в вышеуказанном коде (DOM.setEventListener(widget.getElement(), widget)). Этот вызов нужен для восстановления всех обработчиков событий, установленных для виджета. Все панели GWT, по сути, делают то же самое, инчае новые виджеты теряли бы обработчики событий. Теперь взглянем на код ItemPattern, чтобы понять его роль в общей структуре.

Класс ItemPattern реализует интерфейс Pattern только одним способом. Этот способ:
String generatePattern(Context context);
Несложно догадаться, что результатом этого метода должен являться исходный HTML-код компонента. Занимательная вещь: параметр context. Основной класс FastContainer перекладывает на Pattern работу по созданию параметров, относящихся к исходному HTML-коду. Каждый из этих параметров может быть рассмотрен как способ доступа к некоему элементу в DOM.

  public class ItemPattern implements Pattern {    public String generatePattern(Context context) {  		StringBuffer html = new StringBuffer();    		html.append("<table class="item" cellspacing="0">");    		html.append("<tbody><tr>");  		html.append("<td class="item-leftTop"> </td>");  		html.append("<td class="item-top"> </td>");  		html.append("<td class="item-rightTop"> </td>");  		html.append("</tr>");    		html.append("<tr>");  		html.append("<td class="item-leftMiddle"> </td>");  		html.append("<td class="item-contentCell">");  		int id = Parameter.nextId();  		Parameter checkBoxParam = new Parameter("checkBox", id);  		Parameter titleParam = new Parameter("title", id);  		Parameter kickOffParam = new Parameter("kickOff", id);  		Parameter doByParam = new Parameter("doBy", id);    		context.addParameter(checkBoxParam);  		context.addParameter(titleParam);  		context.addParameter(kickOffParam);  		context.addParameter(doByParam);    		html.append("<table cellspacing="0">");  		html.append("<tbody><tr>");  		html.append("<td> </td>");

Этот фрагмент кода класса ItemPattern показывает суть процесса генерации исходного HTML. Сам по себе HTML генерируется с помощью класса StringBuffer , входящего в состав GWT. Каждый элемент HTML (превращающийся в DOM-элемент после присоединения), предназначенный для слияния с виджетом, ассоциируется с соответствующим Parameter, принимающим имя элемента (значение, необходимое для доступа к элементу методом setWidget(String fieldName, Widget widget) в FastContainer) и числовое значение, уникальное для каждого экземпляра FastContainer. В нашем случае это реализуется статическим методом nextId в классе Parameter, но в реальном приложении возможен и более сложный подход. Важно заметить, что Parameter генерирует уникальный идентификатор для поля. Например, если у нас Parameter описан как new Parameter(“title”, 12), то метод getId() возвратит “title_12”, что и является уникальным значением для этого поля в DOM. Число 12 в данном случае является номером экземпляра FastContainer. Т.о., когда мы вызываем getParameter(“title”) из экземпляра Context, то получаем этот самый уникальный идентификатор.

Это были важные аспекты реализации FastContainer. Для ещё более детального ознакомления Вы можете взять полный исходный код.

Ниже приведена таблица результатов теста для трёх популярных браузеров.

браузер

стандартно, ms

FastContainer, ms

пустые элементы

с управлением

пустые элементы

с управлением

Firefox 2.0.0.12 (Windows)

730

1300

265

935

Internet Explorer 6

702

1100

171

686

Safari 3.0.3 (Windows)

280

436

94

250

Все результаты получены на одном и том же ПК. Вдаваться в его технические характеристики нет смысла, поскольку наша цель – продемонстрировать скоростные различия двух способов создания компонентов в GWT при прочих равных. При взгляде на результаты несложно убедиться, что время, затраченное на создание и присоединение элементов управления в обоих случаях практически одинаково. 570 и 670 мсек для Firefox, 398 и 515 мсек для IE, 156 мсек в обоих случаях для Safari. Незначительная разница (в пределах 100 мсек) может быть списана на использование DOM.getElementById(param.getId()) в FastContainer для поиска целевого элемента. Предположим, что этот метод начинает работать медленнее с ростом числа элементов в DOM. Значит, должны быть способы улучшить ситуацию, начиная поиск целевого элемента с компонентных элементов. Интересным в таблице результатов представляется то, что пустые компоненты (только фреймы, без управляющих элементов) создаются в несколько раз быстрее. Особенно это справедливо для IE (4.1 раза). Это означает, что если у нас будут компоненты со сложной внутренней структурой (которая может быть реализована с помощью FastContainer как HTML-код), то разница в скорости рендеринга будет ещё более впечатляющей.

The results of this simple test showed performance benefits of using setInnerHtml for building GWT components. As we already said before, rendering speed can be essentially increased by using this approach. FastContainer pattern is one of the concrete techniques for building components based on this phenomenon that we can offer to use in real applications development. Of course, it is possible to use setInnerHtml as is to build something you may want, but talking about the pattern we emphasize that it is exactly a container supposed to have an ability to contain GWT widgets inside. It also allows accessing these widgets during the component’s life.

В завершение хочется заметить, что идея FastContainer не так уж и нова. Уже существуют такие вещи, как HTMLTemplateWidget, собирающий виджеты из исходного HTML-кода. В будущих релизах команда GWT также анонсировала GWT Declarative UI. Ждём, когда можно будет опробовать это всё вживую. Цель этого примера – осветить вопрос выигрыша в производительности и предложить работающее техническое решение, годное для использования в разработке на GWT.

Вам понравилась статья, и Вы хотите заказать у нас разработку Вашего проекта? Свяжитесь с нами прямо сейчас!