воскресенье, 14 февраля 2010 г.

История с GWT

Если кто не знает, GWT — это не только компилятор Явы в Яваскрипт, но и библиотека виджетов, сериализация-десериализация RPC клиентом и на сервере, плагин для IDE Eclipse, собственные (native) методы на Яваскрипте. Компоненты более-менее независимые, главное там — компилятор. Хочешь — используй стороннюю библиотеку виджетов, хочешь — иную IDE (Идея рулез). По сравнению с GWT проект Google App Engine выглядит брендовой песочницей. С помощью GWT сам Гугол построил интерфейс Adwords и «Волну». Есть аналогичный набор инструментов для компилирования сырого Яваскрипта в оптимизированный Яваскрипт, о котором хорошо написал Илья Кантор. Между прочим, после оформления Closure Tools в отдельный проект и создания «Волны», я было решил, что Гугол не считает коммерчески обоснованным далее скрывать код инструментов разработки на сыром Яваскрипте. Однако судя по косвенным признакам, Google Bazz написан скорее с помощью Closure Tools, а не GWT, и хоронить Closure Tools рано.

Пример работы GWT со спрайтами. Кратко, суть спрайтов в том, что много мелких картинок собирают в одну, чтобы уменьшить число HTTP запросов для отрисовки страницы (пример). Когда рисуют страницу, средствами CSS кусочек картинки-агрегата подставляют фоном к нужному элементу, и выглядит это точь-в-точь как оригинальное единичное изображение. На практике: собираете мелкие картинки в графическом редакторе, копаетесь в получившемся винегрете, высчитывая координаты составляющих кадров, переносите данные в CSS, проверяете в браузере. Для команды специально обученных верстальщиков это просто. А если потом нужно добавить ещё одну картинку, повторям снова. Ну, вы поняли. В GWT это выглядит следующим образом:

1. утилитой генерируем интерфейс для стилей (назовём его Style)

2. вручную набиваем другой интерфейс: ресурс, где связываем интерфейс стилей со стилями, а произвольный геттер с исходной картинкой.
public interface Resources extends ClientBundle {

Resources RESOURCES = GWT.create(Resources.class);

@Source("layout.css")
Style style();

@Source("images/appLogoSmall.png")
ImageResource appLogoSmall();
}
3. Возвращаемся в стили и прописываем там спрайт:
@sprite .logo {

gwt-image: "appLogoSmall";
position: relative;
top: 18px;
}
4. Используем генерированный класс во время выполнения программы, ссылаясь на метод


Resources.RESOURCES.style.logo()

или в шаблоне GWT UIBinder (отдельная тема)


<ui:with field='res' type='za.co.jigsaw.saas.gwt.resources.client.Resources'/>
<!-- Здесь навесим логотип: -->
<div class='${res.style.logo}>


компилятор сам сгенерирует нужные параметры классу .logo, чтобы он отображал картинку, а картинки соберёт в одну. У выходных файлов (картинка, CSS) имя составлено как хэш-функция от содержимого, что позволяет кэшировать их в браузере навечно.

На пробу выполнил четыре шага для единственного спрайта. Каково же было моё удивление, когда в файербаге:


url(data:image/png;base64,iVBORw0KGgoA...

увидел, что компилятор встроил картинку в файл CSS! Трюк, возможный в Лиске, но не в ie6/7, для которых генерируются свои CSS. Если специально обученные верстальщики могут полениться сравнить: встроенная картинка в случае FF или картинка-агрегат, то компилятор выбирает оптимальный вариант. Внимание разработчиков GWT к деталям производит впечатление.

Это была преамбула, а теперь — амбула. Проект — SaaS с адвордовским пользовательским интерфейсом. Как в теме поста, надо научить дружить историю браузера с GWT. История — это «вперёд», «назад» и перезагрузка страницы (либо её загрузка по урлу). GWT работает с историей на низком уровне, а надо привязать её к архитектуре MVP. Известно два варианта: один изложен в предыдущей ссылке а другой — GWT Presenter (сторонняя библиотека под GWT). Второй сейчас используется в проекте. На низком уровне, история оперирует токенами. Токен — это кусок урла, который идёт после знака ’#’. Изменения токенов отслеживается.

Подробнее про GWT Presenter. Веб-приложение разбито на страницы. Хотя GWT приложение и помещается на одной странице, GWT Presenter разбивает его на места (Places), по которым можно перемещаться с помощью навигации, меняя токены. Грубо говоря, каждому месту соответствует своё представление (view), которое управляется презентатором (presenter). Похоже на странички.
Токены используются для того, чтобы:
1) попасть в нужное место
2) хранить состояние презентатора, чтобы по внешней ссылке или при обновлении странички попасть в нужное состояние приложения.

Пример 1:
Переход из списка элементов к редактору элемента. Презентатор списка выставляет презентатору редактора идентификатор (здесь 2010:3) и вызывает его revealDisplay(), что приводит к обмену событиями по системной шине:
...: «Презентатор, покажись!»
Презентатор: «Я показался!»
Место: «Это мой презентатор? Ага, я показалось!»
Местный управляющий: «Какое-то место показалось... давай токен.»
Место: «Презентатор, давай своё состояние.»
Презентатор: «Идентификатор сейчас ’2010:3’»
Место: «Ясно, значит токен будет ’#settings/lifecycles/edit/;lid=2010:3′. Возвращаю токен.»
Местный управляющий: «А что, в истории по-другому? Ладно, заношу токен в историю.»

Пример 2:
Переход в редактор по ссылке из браузера. Ссылка ведёт на страничку с токеном
#settings/lifecycles/edit/;lid=2010:*

Происходит обмен событиями по системной шине:
Местный управляющий: «Чёрт, история поменялась. Запросили место для ’settings/lifecycles/edit/’!»
Место: «Это что, про меня, что-ли? Щас обработаю. Презентатор, твой идентификатор теперь ’2010:*’, понятно?»
Презентатор: «Угу, законфигурировал.»
Место: «Презентатор, покажись!»
... далее см. пример 1...
Место: «Я показалось»
... опять см. пример 1...

Пример 3:
Перенос изменения состояния презентатора в токен (и историю браузера), чтобы при обновлении странички состояние осталось прежним. Пользователь меняет значение фильтра с 2009 на 2010 год, и начинается обмен сообщениями по системной шине:
Презентатор: «Я поменялся!»
Место: «Это мой презентатор? Я поменялось!»
Местный управляющий: «А тебя сейчас видно? Ага, понятно. Давай токен.»
Место: «Презентатор, фильтр теперь какой?»
Презентатор: «Вот, 2010»
Место: «Ясно, значит токен будет ’#settings/lifecycles/;year=2010′. Возвращаю токен.»
Местный управляющий: «А что, в истории по-другому? Ладно, заношу токен в историю.»

Если сравнить работу GWT Presenter’а с каркасом веб приложения, выходит, что компоненты маршрутизации (routing) и декодирования запроса/кодирование ответа представлены в целом неплохо. Обратная маршрутизация неявно подразумевает, что место для презентатора, который покажут, уже создано. Недостатки: использование сообщений/обработчиков сверх меры. При переходе по ссылке (см. Пример 2) нужное место можно вообще находить синхронно. Обработка похожих сообщений («я появился», «я показался») почти совпадает друг с другом. Ещё про недостатки:

Пример 4:
На страничке (в одном месте) расположены два списка: список пользователей и список ролей. Оба списка можно фильтровать: пользователей по флагу активен/неактивен, а роли по статусу админ/просто пользователь. Логично сделать два отдельных презентатора, каждый из которых наследует функциональность базового презентатора списков. А архитектура GWT Presenter ставит презентатор и место в соответствие один-к-одному. В результате здесь требуется добавить избыточный презентатор, который будет делегировать передачу состояния от места к презентаторам пользователей и ролей и наоборот, а самое неуклюжее — передачу события «я поменялся» на системную шину, потому что место не желает следить за событиями чужих презентаторов.

То, что в данный конкретный момент времени каждый презентатор относится только к одному месту, выглядит разумным. Это требование соответствует отношению много-к-одному. Так было бы удобнее, чем в текущей реализации.

В конце мая пройдёт традиционная конференция Google IO, на котором заявлены аж два доклада по архитектуре GWT приложений. Будем надеятся, докладчики просветят в области архитектуры, типичной для GWT приложений. Пока что буду переделывать GWT Presenter. Попробовать что-ли обратные вызовы?