Пишемо і налагоджує компонент для GWT і Vaadin

image

Фреймворки 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 інструменти, а це велика перевага при розробці масштабних проектів.

Пишемо компонент
image
Сам компонент буде досить простим — поле рейтингу у вигляді 5 зірок. Це поле вводу, в якому користувач вибирає рейтинг за допомогою миші. У нього є стан на сервері і відображення має змінюватися при його зміні.

image
Ось так виглядає наш новий модуль web-toolkit у вікні проекту Idea. У кореневому пакеті розташований описувач GWT модуля.
AppWidgetSet.gwt.xml
<?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:
RatingFieldWidget.java
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, наприклад рефлексія і введення-виведення (повну інформацію про сумісність дивись тут: www.gwtproject.org/doc/latest/RefJreEmulation.html).

Визначаємо зовнішній вигляд
Як ви могли помітити, в коді віджета немає згадок зовнішнього вигляду, крім призначення імен стилів ключовим елементам. Цей прийом постійно використовується при розробці компонентів і дозволяє визначати зовнішній вигляд CSS на основі комбінацій стилів.

Для того, щоб визначити зовнішній вигляд нашого компонента, спершу, створимо файли стилів. Для цього можемо скористатися дією 'Create theme extension' теми 'halo'. Ця тема використовує замість іконок гліфи шрифту FontAwesome, чим ми і скористаємося. Studio створить порожні файли SCSS для наших експериментів в каталозі themes модуля web.

Стилі кожного компонента прийнято виділяти в окремий файл componentname.scss в каталозі components/componentname у форматі домішки SCSS:
ratingfield.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;
}
}

Потім такий файл підключається в головному файлі теми
halo-ext.scss
@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:

1) Клас компонента RatingField визначає API для серверного коду, різні get/set методи для роботи, слухачі подій і підключення до джерел даних. Прикладні розробники завжди використовують у своєму коді методи цього класу.
RatingField.java
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;
}
}

2) Клас стану RatingFieldState відповідає за дані, які будуть пересилатися між клієнтом і сервером. У ньому визначаються публічні поля, які будуть автоматично серіалізовать на сервері і десериализованы на клієнта.
RatingFieldState.java
package com.haulmont.ratingsample.web.toolkit.ui.client;

import com.vaadin.shared.AbstractFieldState;

public class RatingFieldState extends AbstractFieldState {
{ // змінимо головне ім'я стилю компонента
primaryStyleName = "ratingfield";
}
// оголосимо поле для нашого значення
public int value = 0;
}

3) Інтерфейс RatingFieldServerRpc — визначає API сервера для клієнтської частини, його методи можуть викликатися з клієнта за допомогою механізму віддаленого виклику процедур вбудованого в Vaadin. Цей інтерфейс ми реалізуємо в самому компоненті, в даному випадку просто викликаємо метод setValue нашого поля.
RatingFieldServerRpc.java
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), так і задається відповідність клієнтської частини серверної:
RatingFieldConnector.java
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);
}
}
}

Пробний запуск
Щоб все це спробувати в справі, виконаємо кілька підготовчих кроків:

1) Створимо БД для програми з меню Studio: Run — Create database
2) Створимо екран для розміщення компонента в модулі web:

image
3) Додамо екран меню додатка: Main menu — Edit

image
4) Тепер перейдемо до редагування нашого екрану в IDE.
Нам знадобиться контейнер для нашого компонента, давайте оголосимо його в XML екрану:
rating-screen.xml
<?xml version="1.0" encoding="UTF-8" standalone="ні"?>
<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 і додамо код розміщення нашого компонента на екрані:
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 дивитися результат:

image
Радіємо повнофункціональному компоненту, який ми тепер можемо використовувати з нашого 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-код в браузері, але з деякими обмеженнями.

Схема роботи така:
  1. Ви запускаєте сервер com.google.gwt.dev.codeserver.CodeServer віддає на сторону браузера відповідність JS-коду та коду Java, а також збирає ваш виджетсет при оновленні сторінки
  2. Відкриваєте додаток з параметрами ?debug&superdevmode
  3. Налаштовуєте Developer Tools, F12, в нижньому правому куті є кнопка відкриття налаштувань. Відзначити опцію Enable source maps
  4. Обновіть сторінку, відкрийте в Developer Tools вкладку Sources. Там повинні бути показані всі Java класи GWT віджетів. В налагоджувач Chrome можна ставити точки зупину, дивитися змінні і виконувати вираження.
  5. При зміні коду віджета в проекті достатньо оновити сторінку, виджетсет буде пересобран і підхоплений браузером. Це дозволяє на льоту бачити зміни в код віджету, що помітно прискорює розробку.
Пробуємо всі запустити в нашому проекті:
1) Для запуску цього режиму нам необхідно додати runtime залежність servletApi для модуля web-toolkit у файлі build.gradle:
build.gradle
...
configure(webToolkitModule) {
dependencies {
...
runtime(servletApi)
}
...

2) Виконаємо Studio дія 'Create or update IDE project files', щоб Idea побачила нову залежність
3) Створюємо нову конфігурацію запуску в 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 необхідно замінити на свої.

image
4) Виконуємо в Studio: Run-Start application server
5) Запускаємо раніше створену конфігурацію GWT в Idea
6) Переходимо за адресою http://localhost:8080/app?debug&superdevmode
7) Відкриваємо DevTools Chrome і бачимо свій Java код:

image
Плюс цього способу в тому, що він не потребує особливої підтримки від IDE, швидко працює і дозволяє налагоджувати код прямо в браузері. До мінусів варто віднести те, що вам недоступно виконання Java коду під час налагодження, а також точки зупину з умовами на Java, так і незвично якось. Є ще жирний мінус — старі браузери зовсім не вміють source-maps, що ускладнює нормальну налагодження.

Замість висновку
GWT — дуже сильний і розвинений веб-фреймворк, в останні роки він активно використовується великим числом розробників по всьому світу. Google не забуває про своє дітище і активно застосовує його, зовсім нещодавно вони випустили Gmail Inbox (http://gmailblog.blogspot.ua/2014/11/going-under-hood-of-inbox.html), який інтенсивно використовує GWT для веб інтерфейсу.

Vaadin теж не відстає і зараз є одним з кращих варіантів серверної частини для GWT. Сервер-орієнтована модель дозволяє швидше розробляти, простіше супроводжувати програми і менше переживати за безпеку даних. Складність доопрацювання функціоналу GWT і Vaadin досить ізольована і не слід лякатися, переваги цих технологій для розробки перекривають всі їхні мінуси.

Ми активно використовуємо Vaadin ось уже 5 років і впевнені в ньому. Всім раджу розглядати його в якості основного фреймворку для побудови веб-додатків, особливо бізнес спрямованості.

Спасибі за увагу!

Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.