Фреймворки Google Web Toolkit и Vaadin достаточно хорошо себя показывают, когда вы просто используете их. Если вдруг вам понадобилось расширить их функциональность, то вам придётся серьёзно попотеть. В этой статье я хочу рассказать, как написать простой компонент на GWT, добавить к нему серверную часть Vaadin и использовать в своём приложении. Я не буду создавать некий совсем пустой GWT/Vaadin проект и с самого начала настраивать компиляцию GWT, вместо этого возьму приложение на базе CUBA.Platform, в котором и реализую сам компонент, а затем попробую в деле. Так будет видно, насколько хорошо всё впишется в настоящее приложение. Отдельное внимание я бы хотел уделить отладке компонента, поскольку она нетривиальна и всегда вызывает трудности у разработчиков.
Хочу предупредить, что не всё описанное в статье относится к GWT и Vaadin, часть шагов и приёмов применимы только в контексте использования CUBA.Platform, за счёт этого сильно упрощена настройка окружения и некоторые рутинные действия.
Подготовка окружения
Мы будем использовать для опытов пустой проект, созданный в CUBA Studio. CUBA — наша платформа для разработки бизнес приложений на Java, которая позволяет быстро создавать модель данных и интерфейс приложения, определять логику работы с данными и управлять правами пользователей. В основе UI платформы активно используется веб-фреймворк Vaadin, что позволяет нам реализовывать множество интересных задумок.
Для начала создадим модуль GWT, который будет компилироваться в JavaScript. Выполним действие 'Create web toolkit module' в Studio. Это простая вспомогательная операция, которую нет смысла выполнять вручную. Studio сгенерирует описатель GWT модуля AppWidgetSet.gwt.xml, директорию для модуля и пустой пакет, а также добавит в описание сборки build.gradle необходимые задачи.
Следующим шагом запустим действие 'Create or update IDE project files', чтобы сгенерировать файлы проекта IntelliJ IDEA, и отправимся писать код компонента в IDE.
Для программирования самого компонента нам не потребуется каких-то особенных возможностей IDE кроме подсветки Java кода, поэтому совсем необязательно использовать IntelliJ Idea, с таким же успехом мы можем использовать Eclipse или Netbeans. Благодаря Google Web Toolkit мы можем использовать знакомые Java инструменты, а это большое преимущество при разработке масштабных проектов.
Пишем компонент
Сам компонент будет довольно простым — поле рейтинга в виде 5 звёзд. Это поле ввода, в котором пользователь выбирает рейтинг при помощи мыши. У него есть состояние на сервере и отображение должно меняться при его изменении.
Вот так выглядит наш новый модуль web-toolkit в окне проекта Idea. В корневом пакете расположен описатель GWT модуля.
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE module PUBLIC "-//Google Inc.//DTD Google Web Toolkit 1.7.0//EN" "http://google-web-toolkit.googlecode.com/svn/tags/1.7.0/distro-source/core/src/gwt-module.dtd">
<module>
<inherits name="com.haulmont.cuba.web.toolkit.ui.WidgetSet" />
</module>
Он наследует базовый модуль CUBA.Platform и является отправной точкой всей клиентской части нашего приложения (той, что исполняется в браузере). По умолчанию реализации GWT компонентов должны располагаться в подпакете 'client'. Создадим пакет client и в нём подпакет ratingfield.
Первая деталь нашего компонента — виджет GWT. Класс RatingFieldWidget, который мы разместим в модуле web-toolkit:
package com.haulmont.ratingsample.web.toolkit.ui.client.ratingfield;
import com.google.gwt.dom.client.DivElement;
import com.google.gwt.dom.client.SpanElement;
import com.google.gwt.dom.client.Style.Display;
import com.google.gwt.user.client.DOM;
import com.google.gwt.user.client.Event;
import com.google.gwt.user.client.ui.FocusWidget;
import java.util.ArrayList;
import java.util.List;
// класс GWT виджета
public class RatingFieldWidget extends FocusWidget {
private static final String CLASSNAME = "ratingfield";
// API для реакции на клики
public interface StarClickListener {
void starClicked(int value);
}
protected List<SpanElement> stars = new ArrayList<SpanElement>(5);
protected StarClickListener listener;
protected int value = 0;
public RatingFieldWidget() {
DivElement container = DOM.createDiv().cast();
container.getStyle().setDisplay(Display.INLINE_BLOCK);
for (int i = 0; i < 5; i++) {
SpanElement star = DOM.createSpan().cast();
// Добавляем элемент звезды в контейнер
DOM.insertChild(container, star, i);
// Подписываемся на событие ONCLICK
DOM.sinkEvents(star, Event.ONCLICK);
stars.add(star);
}
setElement(container);
setStylePrimaryName(CLASSNAME);
}
// главный метод обработки событий в виджетах GWT
@Override
public void onBrowserEvent(Event event) {
super.onBrowserEvent(event);
switch (event.getTypeInt()) {
// Реагируем на события ONCLICK
case Event.ONCLICK:
SpanElement element = event.getEventTarget().cast();
// только если клик по звезде
int index = stars.indexOf(element);
if (index >= 0) {
int value = index + 1;
// устанавливаем внутреннее значение
setValue(value);
// уведомляем интересующихся
if (listener != null) {
listener.starClicked(value);
}
}
break;
}
}
// Понадобиться если на сервере зададут другой primaryStyleName
// это часто случается при наследовании классов компонентов
@Override
public void setStylePrimaryName(String style) {
super.setStylePrimaryName(style);
for (SpanElement star : stars) {
star.setClassName(style + "-star");
}
updateStarsStyle(this.value);
}
// Позволим изменять состояние стороннему коду
public void setValue(int value) {
this.value = value;
updateStarsStyle(value);
}
// обновляем визуальное представление
private void updateStarsStyle(int value) {
for (SpanElement star : stars) {
star.removeClassName(getStylePrimaryName() + "-star-selected");
}
for (int i = 0; i < value; i++) {
stars.get(i).addClassName(getStylePrimaryName() + "-star-selected");
}
}
}
Виджет представляет собой изолированный класс, отвечающий за отображение и реакцию на события. Он не должен знать о серверной части, он лишь определяет интерфейсы для работы с ней. В нашем случае это метод setValue и интерфейс StarClickListener.
Стоит отметить, что во всём коде виджета нет ни строки JavaScript, что довольно хорошо для больших и сложных компонентов. Но не забывайте, что этот Java код будет скомпилирован в JavaScript и вам могут быть недоступны многие части стандартной библиотеки Java, например рефлексия и ввод-вывод (полную информацию о совместимости смотри тут).
Определяем внешний вид
Как вы могли заметить, в коде виджета нет упоминаний внешнего вида, кроме назначения имён стилей ключевым элементам. Этот приём постоянно используется при разработке компонентов и позволяет определять внешний вид в CSS на основе комбинаций стилей.
Для того, чтобы определить внешний вид нашего компонента, сперва, создадим файлы стилей. Для этого можем воспользоваться действием 'Create theme extension' для темы 'halo'. Эта тема использует вместо иконок глифы шрифта FontAwesome, чем мы и воспользуемся. Studio создаст пустые файлы SCSS для наших экспериментов в каталоге themes модуля web.
Стили каждого компонента принято выделять в отдельный файл componentname.scss в каталоге components/componentname в формате примеси SCSS:
@mixin ratingfield($primary-stylename: ratingfield) {
.#{$primary-stylename}-star {
font-family: FontAwesome;
font-size: $v-font-size--h2;
padding-right: round($v-unit-size/4);
cursor: pointer;
&:after {
content: '\f006'; // 'fa-star-o'
}
}
.#{$primary-stylename}-star-selected {
&:after {
content: '\f005'; // 'fa-star'
}
}
.#{$primary-stylename} .#{$primary-stylename}-star:last-child {
padding-right: 0;
}
.#{$primary-stylename}.v-disabled .#{$primary-stylename}-star {
cursor: default;
}
}
Затем такой файл подключается в главном файле темы.
@import "../halo/halo";
@import "components/ratingfield/ratingfield";
/* Define your theme modifications inside next mixin */
@mixin halo-ext {
@include halo;
@include ratingfield;
}
Пара слов об SCSS. Это формат описания CSS стилей, позволяющий использовать переменные, примеси и вычисляемые значения. Он активно используется во многих веб-фреймворках, в Vaadin 7 — это базовый формат тем приложения. В приложении CUBA.Platform мы можем просто использовать этот формат, поскольку Studio берёт на себя грязную работу по организации сборки тем SCSS.
Формат описания в виде примеси поможет нам, если у компонента появятся наследники с другим primary-stylename. Мы просто включим стили предка при помощи SCSS include.
Для наших звёзд мы используем два глифа FontAwesome — 'fa-star' и 'fa-star-o'. Сам CSS довольно прост и включает лишь символы звёзд в двух состояниях и курсор мыши для них.
Для переключения темы в приложении нужно выбрать halo на странице Project Properties в CUBA.Studio.
Добавляем серверную часть
До этого момента мы могли использовать написанный нами виджет в каком-нибудь GWT приложении, поскольку он никак не зависел от сервера. Теперь давайте поговорим про фреймворк Vaadin и его сервер-ориентированную модель. Она имеет пару особенностей:
— всё состояние компонентов и полей ввода хранится на сервере и может быть восстановлено даже после полного обновления страницы или обрыва связи с сервером
— весь полезный код приложения, за исключением клиентской части, исполняется на сервере
То есть компоненты Vaadin скрывают от разработчика то, как они работают в браузере и беспечный Java разработчик HTML/CSS никогда и не видит (ну или почти никогда не видит, а то вдруг ещё полезет писать компоненты).
Создадим пакет com.haulmont.ratingsample.web.toolkit.ui в модуле web. В нём мы разместим код нашего компонента RatingField. Унаследуем класс от класса Vaadin AbstractField, определяющего базовую логику работы полей ввода.
Ключевые серверные составляющие компонента Vaadin:
- Класс компонента RatingField определяет API для серверного кода, различные get/set методы для работы, слушатели событий и подключение источников данных. Прикладные разработчики всегда используют в своём коде методы этого класса.
package com.haulmont.ratingsample.web.toolkit.ui;
import com.haulmont.ratingsample.web.toolkit.ui.client.RatingFieldServerRpc;
import com.haulmont.ratingsample.web.toolkit.ui.client.RatingFieldState;
import com.vaadin.ui.AbstractField;
// Наше поле будет иметь тип значения Integer
public class RatingField extends AbstractField<Integer> {
public RatingField() {
// регистрируем особую реализацию интерфейса, которая будет вызвана при запросе с клиента
registerRpc(new RatingFieldServerRpc() {
@Override
public void starClicked(int value) {
setValue(value, true);
}
});
}
// тип значения поля
@Override
public Class<? extends Integer> getType() {
return Integer.class;
}
// определяем свой класс для состояния
@Override
protected RatingFieldState getState() {
return (RatingFieldState) super.getState();
}
@Override
protected RatingFieldState getState(boolean markAsDirty) {
return (RatingFieldState) super.getState(markAsDirty);
}
// при вызове setValue из прикладного кода нужно обновить состояние
@Override
protected void setInternalValue(Integer newValue) {
super.setInternalValue(newValue);
if (newValue == null) {
newValue = 0;
}
getState().value = newValue;
}
}
- Класс состояния RatingFieldState отвечает за то, какие данные будут пересылаться между клиентом и сервером. В нём определяются публичные поля, которые будут автоматически сериализованы на сервере и десериализованы на клиенте.
package com.haulmont.ratingsample.web.toolkit.ui.client;
import com.vaadin.shared.AbstractFieldState;
public class RatingFieldState extends AbstractFieldState {
{ // изменим главное имя стиля компонента
primaryStyleName = "ratingfield";
}
// объявим поле для нашего значения
public int value = 0;
}
- Интерфейс RatingFieldServerRpc — определяет API сервера для клиентской части, его методы могут вызываться с клиента при помощи механизма удалённого вызова процедур встроенного в Vaadin. Этот интерфейс мы реализуем в самом компоненте, в данном случае просто вызываем метод setValue нашего поля.
package com.haulmont.ratingsample.web.toolkit.ui.client;
import com.vaadin.shared.communication.ServerRpc;
public interface RatingFieldServerRpc extends ServerRpc {
// метод будет вызываться в клиентском коде
void starClicked(int value);
}
Важный момент — классы состояния и rpc должны быть расположены в подпакете 'client', так цепкие лапы компилятора GWT без проблем доберутся до них, чтобы создать их JavaScript представление для клиентского кода. Помимо этого, классы не должны использовать код, который не может быть скомпилирован GWT.
Вот теперь настал момент связать наш клиентский код с серверной частью. Эту роль в Vaadin выполняют классы-коннекторы. Они размещаются рядом с классами виджетов. Класс коннектора аннотируется Connect(ComponentName.class), так и задаётся соответствие клиентской части серверной:
package com.haulmont.ratingsample.web.toolkit.ui.client.ratingfield;
import com.haulmont.ratingsample.web.toolkit.ui.RatingField;
import com.haulmont.ratingsample.web.toolkit.ui.client.RatingFieldServerRpc;
import com.haulmont.ratingsample.web.toolkit.ui.client.RatingFieldState;
import com.vaadin.client.communication.StateChangeEvent;
import com.vaadin.client.ui.AbstractFieldConnector;
import com.vaadin.shared.ui.Connect;
// Связываем наш коннектор с серверной реализацией RatingField
// наследуем коннектор для AbstractField
@Connect(RatingField.class)
public class RatingFieldConnector extends AbstractFieldConnector {
// мы будем использовать виджет RatingFieldWidget
@Override
public RatingFieldWidget getWidget() {
RatingFieldWidget widget = (RatingFieldWidget) super.getWidget();
if (widget.listener == null) {
widget.listener = new RatingFieldWidget.StarClickListener() {
@Override
public void starClicked(int value) {
getRpcProxy(RatingFieldServerRpc.class).starClicked(value);
}
};
}
return widget;
}
// наш тип состояния - RatingFieldState
@Override
public RatingFieldState getState() {
return (RatingFieldState) super.getState();
}
// реагируем на изменение состояния на сервере
@Override
public void onStateChanged(StateChangeEvent stateChangeEvent) {
super.onStateChanged(stateChangeEvent);
// если значение на сервере изменилось, обновляем виджет
if (stateChangeEvent.hasPropertyChanged("value")) {
getWidget().setValue(getState().value);
}
}
}
Пробный запуск
Чтобы всё это попробовать в деле, выполним несколько подготовительных шагов:
- Создадим БД для приложения из меню Studio: Run — Create database
- Создадим экран для размещения компонента в модуле web:
- Добавим экран в меню приложения: Main menu — Edit
- Теперь перейдём к редактированию нашего экрана в IDE.
Нам понадобится контейнер для нашего компонента, давайте объявим его в XML экрана:
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<window xmlns="http://schemas.haulmont.com/cuba/5.3/window.xsd"
caption="msg://caption"
class="com.haulmont.ratingsample.web.RatingScreen"
messagesPack="com.haulmont.ratingsample.web">
<layout expand="container">
<vbox id="container">
<!-- вот сюда мы добавим наш компонент Vaadin -->
</vbox>
</layout>
</window>
Откроем класс контроллера экрана RatingScreen.java и добавим код размещения нашего компонента на экране:
package com.haulmont.ratingsample.web;
import com.haulmont.ratingsample.web.toolkit.ui.RatingField;
import com.haulmont.cuba.gui.components.AbstractWindow;
import com.haulmont.cuba.gui.components.BoxLayout;
import com.haulmont.cuba.web.gui.components.WebComponentsHelper;
import javax.inject.Inject;
import java.util.Map;
public class RatingScreen extends AbstractWindow {
@Inject
private BoxLayout container;
@Override
public void init(Map<String, Object> params) {
super.init(params);
// используем API CUBA чтобы добраться до Vaadin реализации контейнера:
com.vaadin.ui.Layout containerLayout = WebComponentsHelper.unwrap(container);
// используем наш компонент как в обычном Vaadin приложении:
RatingField field = new RatingField();
field.setCaption("Rate this!");
containerLayout.addComponent(field);
}
}
Модуль Web отлично поддерживает интеграцию компонентов Vaadin, как сторонних, так и самописных. Вы можете напрямую использовать их, как если бы писали приложение на чистом Vaadin.
Запускаем приложение из Studio: Start application server, переходим на http://localhost:8080/app смотреть результат:
Радуемся полнофункциональному компоненту, который мы теперь можем использовать из нашего Java кода на сервере. Весь код пригоден для использования в любом Vaadin приложении.
Полный код приложения можно найти тут: github.com/Haulmont/ratingsample.git
Отладка в браузере
Мы будем рассматривать только отладку кода виджетов, поскольку отлаживать код Java компонентов на сервере довольно просто.
Отладка любого GWT кода совсем нетривиальна и требует аккуратности. Для отладки воспользуемся режимом SuperDevMode. Необходимо, чтобы ваш проект собирался с GWT 2.5.1 или старше. Этот режим предполагает использование сопоставления Java кода с JavaScript кодом в браузере (source-maps, см developer.chrome.com/devtools/docs/javascript-debugging#source-maps). То есть вы будете видеть и отлаживать Java код в браузере, но с некоторыми ограничениями.
Схема работы такая:
- Вы запускаете сервер com.google.gwt.dev.codeserver.CodeServer отдающий на сторону браузера соответствие JS кода и Java кода, а также собирающий ваш виджетсет при обновлении страницы
- Открываете приложение с параметрами ?debug&superdevmode
- Настраиваете Developer Tools, F12, в нижнем правом углу есть кнопка открытия настроек. Отметить опцию Enable source maps
- Обновите страницу, откройте в Developer Tools вкладку Sources. Там должны быть показаны все Java классы GWT виджетов. В отладчике Chrome можно ставить точки останова, смотреть переменные и выполнять выражения.
- При изменении кода виджета в проекте достаточно обновить страницу, виджетсет будет пересобран и подхвачен браузером. Это позволяет на лету видеть изменения кода виджета, что заметно ускоряет разработку.
Пробуем всё запустить в нашем проекте:
- Для запуска этого режима нам необходимо добавить runtime зависимость servletApi для модуля web-toolkit в файле build.gradle:
...
configure(webToolkitModule) {
dependencies {
...
runtime(servletApi)
}
...
- Выполним в Studio действие 'Create or update IDE project files', чтобы Idea увидела новую зависимость
- Создаём новую конфигурацию запуска в Idea с типом Application и следующими параметрами:
Main class: com.google.gwt.dev.codeserver.CodeServer
VM options: -Xmx512M
Use classpath of module: app-web-toolkit
Program arguments: -workDir C:\Users\yuriy\work\ratingsample\build\tomcat\webapps\app\VAADIN\widgetsets -src C:\Users\yuriy\work\ratingsample\modules\web\src -src C:\Users\yuriy\work\ratingsample\modules\web-toolkit\src com.haulmont.ratingsample.web.toolkit.ui.AppWidgetSet
Пути к каталогам build\tomcat\webapps\app\VAADIN\widgetsets, modules\web\src и modules\web-toolkit\src необходимо заменить на свои.
- Выполняем в Studio: Run-Start application server
- Запускаем ранее созданную конфигурацию GWT в Idea
- Переходим по адресу http://localhost:8080/app?debug&superdevmode
- Открываем DevTools в Chrome и видим свой Java код:
Плюс этого способа в том, что он не требует особой поддержки от IDE, быстро работает и позволяет отлаживать код прямо в браузере. К минусам стоит отнести то, что вам недоступно выполнение Java кода во время отладки, а также точки останова с условиями на Java, да и непривычно это как-то. Есть ещё жирный минус — старые браузеры совсем не умеют source-maps, что затрудняет нормальную отладку.
Вместо заключения
GWT — очень сильный и развитый веб-фреймворк, в последние годы он активно используется большим числом разработчиков по всему миру. Google не забывает про своё детище и активно применяет его, совсем недавно они выпустили Gmail Inbox (http://gmailblog.blogspot.ru/2014/11/going-under-hood-of-inbox.html), который интенсивно использует GWT для веб интерфейса.
Vaadin тоже не отстаёт и сейчас является одним из лучших вариантов серверной части для GWT. Сервер-ориентированная модель позволяет быстрее разрабатывать, проще сопровождать приложения и меньше переживать за безопасность данных. Сложность доработки функционала GWT и Vaadin довольно изолирована и не следует её пугаться, преимущества этих технологий для разработки перекрывают все их минусы.
Мы активно используем Vaadin вот уже 5+ лет и уверены в нём. Всем советую рассматривать его, в качестве основного фреймворка для построения веб-приложений, особенно бизнес направленности.
Спасибо за внимание!
Автор: Юрий Артамонов