Домашняя бухгалтерия на платформе CUBA. Часть 2

в 12:04, , рубрики: enterprise, java, Блог компании Haulmont, корпоративные приложения, Платформа, разработка

Домашняя бухгалтерия на платформе CUBA. Часть 2 - 1

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

В первой части я рассказал об основных частях приложения: модели данных, бизнес-логике среднего слоя и экранах, созданных на технологии Generic UI платформы. Во второй части, как и обещал, расскажу о том, как сменить тему Generic UI, как изменить поведение визуального компонента, и опишу устройство дополнительного UI для мобильных устройств, написанного на JavaScript.

Для начала напомню, какие задачи решает приложение:

  1. На любой момент времени показывает текущий баланс по всем видам денежных средств: наличные, карты, депозиты, долги и т.д.
  2. Формирует отчет по категориям доходов и расходов, позволяющий узнать, на что тратились или откуда поступали деньги в определенный период.

Исходный код приложения находится на GitHub, инструкция по сборке и запуску — в предыдущей статье. Платформа CUBA не является свободной, однако пяти одновременных подключений в бесплатной лицензии вполне достаточно для домашнего применения, так что если кто-то захочет использовать — пожалуйста.

Для изучения и доработки приложения рекомендую скачать и установить CUBA Studio, IntelliJ IDEA и плагин CUBA для нее.

Тема оформления UI

Начнем с простого — как сменить и настроить тему оформления основного UI приложения.

Со времени выхода первой части статьи прошло довольно много времени, и мы успели выпустить новую версию платформы — 5.3, в которой добавили новую тему Halo. Она основана на теме Valo фреймворка Vaadin, и вдобавок к тому, что симпатично выглядит и отлично масштабируется, еще и очень легко кастомизируется. В частности, для полной смены цветовой гаммы нужно переопределить только несколько переменных SCSS.

Так что мне было достаточно перевести приложение на новую версию платформы, и теперь пользователь системы может выбрать для себя в окне Help > Settings тему оформления: старую Havana или новую Halo.

В новой теме приложение выглядит так:
Домашняя бухгалтерия на платформе CUBA. Часть 2 - 2

Это стандартные настройки темы. Единственное, что я добавил — стиль чисел итогового баланса в левой панели. Для этого в классе LeftPanel для компонентов Label, используемых для отображения итогов, задано имя стиля: totals. Затем с помощью CUBA Studio я создал расширение темы Halo. При этом в проекте в модуле web появился подкаталог themes/halo, в котором можно переопределить параметры темы. Для определения стиля totals в файл halo-ext.scss добавлен CSS-код:

.v-label-totals {
 font-weight: bold;
}

Имя класса для селектора проще всего определить средствами браузера в работающем приложении.

Тему Halo легко адаптировать на свой вкус. У меня дизайнерские способности отсутствуют, поэтому приведу только пример определения темной цветовой гаммы:
Домашняя бухгалтерия на платформе CUBA. Часть 2 - 3

Для такого изменения темы в файл halo-ext-defaults.scss добавлены следующие значения переменных:

$v-background-color: #444D50;
$v-support-inverse-menu: false;

Аналогично с помощью переменных темы можно изменить размер шрифта различных элементов, отступы, параметры таблиц и т.д.

Интересной особенностью темы Halo является то, что в ней для иконок стандартных компонентов плаформы используются глифы шрифта Font Awesome. Кроме очевидного преимущества при масштабировании это позволяет также автоматически выбирать цвет иконок в зависимости от цвета фона — в примере выше мне не пришлось определять новый набор иконок для темного фона.

Кастомизация визуальных компонентов

Теперь перейдем к возможностям изменения поведения визуальных компонентов, а конкретнее, клиентского кода компонентов, выполняющегося в браузере. Компоненты CUBA в веб-клиенте реализуются на технологии фреймворка Vaadin, поэтому основной метод разработки клиентской части компонентов — использование Java и компиляции в JavaScript при помощи GWT. Это обширная тема и требует отдельной статьи, поэтому здесь я рассмотрю более простой вариант — расширение компонента путем реализации дополнительной логики на JavaScript.

Сформулируем задачу. Сейчас поле суммы операции позволяет вводить арифметическое выражение, которое рассчитывается в контроллере экрана с помощью класса AmountCalculator при сохранении операции. То есть все это происходит на стороне сервера. Задача следующая: по нажатию клавиши “=” в поле суммы производить расчет выражения на клиенте (в браузере) и сразу отображать результат в поле.

Вычислить арифметическое выражение на JavaScript просто: проверить выражение на валидность с помощью регулярного выражения, и затем выполнить через eval(). Главный вопрос — как подключить свой JavaScript к нужному месту клиентского кода приложения? В Vaadin начиная с версии 7 для этого есть специальный механизм расширений компонентов, а конкретнее — класс AbstractJavaScriptExtension. Им я и воспользовался.

В модуле web создан Java-класс CalcExtension, унаследованный от AbstractJavaScriptExtension. Все, что он делает — в конструкторе принимает экземпляр TextField и передает его в унаследованный метод extend(), то есть “расширяет” некоторым своим способом. Кроме того, этот класс имеет аннотацию JavaScript с именем файла, в котором и написана клиентская логика на JavaScript.

// CalcExtension.java
@JavaScript("textfieldcalc.js")
public class CalcExtension extends AbstractJavaScriptExtension {
   public CalcExtension(TextField textField) {
       super.extend(textField);
   }
}

Расширение подключено к компоненту com.vaadin.ui.TextField, полученному из CUBA-компонента TextField в методе initAmount() класса AmountCalculator:

// AmountCalculator.java
public void initAmount(TextField amountField, BigDecimal value) {
	com.vaadin.ui.TextField vTextField = WebComponentsHelper.unwrap(amountField);
	new CalcExtension(vTextField);
	...

Файл textfieldcalc.js расположен в каталоге web/VAADIN модуля web. При сборке приложения он автоматически копируется в каталог VAADIN веб-приложения — там находятся также GWT widgetsets, темы и другие ресурсы. В файле определяется глобальная функция с именем, соответствующим полному имени Java-класса расширения, но с точками замененными на подчеркивания.

// textfieldcalc.js
window.akkount_web_operation_CalcExtension = function() {
   var connectorId = this.getParentId();
   var input = $(this.getElement(connectorId));
   input.on("keypress", function(event) {
       if (event.which == 61) {
           event.preventDefault();
           var x = event.target.value;
           if (x.match(/([-+]?[0-9]*.?[0-9]+[-+*/])+([-+]?[0-9]*.?[0-9]+)/)) {
               event.target.value = eval(x);
           }
       }
   });
}

Эта функция вызывается при инициализации компонента, и this в ней будет указывать на специальный объект, через который осуществляется взаимодействие с фреймворком. Методы этого объекта описаны в JavaDocs на класс AbstractJavaScriptExtension, и с их помощью можно получить DOM-элемент, реализующий компонент, в данном случае — input. Далее на элемент при помощи jQuery навешивается event listener, выполняющий нужные действия. Результат достигнут — при нажатии пользователем клавиши “=” (код 61) выражение, находящееся в поле, рассчитывается и результат проставляется обратно в поле.

Дополнительный UI на JavaScript

В практике разработки приложений на CUBA дополнительный front-end создается обычно для “внешних” пользователей: клиентов, мобильных сотрудников и т.д. Это либо веб-сайты, либо мобильные приложения со специфическим дизайном и требованиями к usability, что диктует применение низкоуровневых нативных технологий. Такой дополнительный UI требует на порядок больше усилий в разработке и поддержке, чем CUBA Generic UI. К счастью, требуемая функциональность дополнительного UI, как правило, значительно меньше, чем функциональность всей информационной системы предприятия.

В описываемом приложении дополнительный responsive UI на Backbone.js + Bootstrap служит для удобства работы на мобильных устройствах. Он позволяет только вводить операции и видеть текущий баланс. Отчет по категориям и управление справочниками не реализованы, фильтрация и сортировка операций тоже.

Домашняя бухгалтерия на платформе CUBA. Часть 2 - 4

Рассмотрим сначала серверную сторону.

В состав CUBA входит модуль portal, который включает в себя кроме прочего универсальный REST API для работы с моделью данных. Через него и идет основное взаимодействие клиентского JavaScript приложения со средним слоем. В проекте создано только два дополнительных метода API: получение текущего баланса и получение последнего использованного счета. Все остальное реализуется универсальным API.

Для использования REST API в своем проекте необходимо создать в нем модуль portal. Проще всего это сделать в CUBA Studio командой Create portal module в секции Project properties. При этом создается также заготовка веб-приложения на Spring MVC, служащая примером аутентификации и работы со средним слоем. В нашем приложении эта заготовка не используется, поэтому все кроме класса PortalController и конфигурационных файлов в корне src удалено.

Класс PortalController является контроллером Spring MVC и содержит два метода: getBalance() и getLastAccount(), вызываемые по HTTP GET. Это и есть методы, дополняющие стандартный REST API платформы, и реализованы они аналогично: сначала аутентификация, затем логика метода, включающая вызов сервисов среднего слоя и формирование результата в JSON. Таким образом, основная логика метода выполняется в контексте пользовательской сессии, ключ которой передан в параметре метода.

// PortalController.java
@Inject
protected Authentication authentication;

@RequestMapping(value = "/api/balance", method = RequestMethod.GET)
public void getBalance(@RequestParam(value = "s") String sessionId,
                   	HttpServletRequest request,
                   	HttpServletResponse response) throws IOException {
	if (!authentication.begin(sessionId)) {
    	response.sendError(HttpServletResponse.SC_UNAUTHORIZED);
    	return;
	}
	try {
    	JSONObject result = new JSONObject();
    	...
    	response.setContentType("application/json;charset=UTF-8");
    	PrintWriter writer = response.getWriter();
    	writer.write(result.toString());
    	writer.flush();
	} catch (Throwable e) {
    	response.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
	} finally {
    	authentication.end();
	}
}

Стандартный REST API платформы включает в себя методы логина и логаута, получения сущностей по ID и по JPQL-запросу, коммит измененных экземпляров — в общем все для CRUD-операций. В совокупностью с двумя специфичными методами класса PortalController этого достаточно для работы клиентского JavaScript приложения.

Перейдем к клиентской стороне дополнительного UI.

Клиентский код расположен в каталоге web/akkount модуля portal. Он загружается в браузер с помощью сервлета PortalDispatcherServlet, конфигурация которого находится в файле portal-dispatcher-spring.xml.

Собственно, приложение состоит из одного хост-файла HTML и набора моделей и представлений Backbone.js. Компоновка и оформление сделаны на Bootstrap, немногочисленный специфический CSS находится в файле css/akkount.css. Скрипт main.js является точкой входа в приложение — он инициализирует роутер Backbone, расположенный в файле router.js. Кроме того, в main.js находятся некоторые общие функции, переменные и константы.

Аутентификация работает следующим образом. После логина полученный с сервера ключ сессии запоминается в переменной скрипта main.js и в sessionStorage. При перезагрузке страницы ключ берется из sessionStorage, cookie не используются. Если при выполнении загрузки операций получена ошибка 401 (ключ сессии невалидный вследствие session expiration на сервере), то происходит перенаправление на view логина. Для постоянного хранения имени пользователя (если установлен флажок в окне логина) используется localStorage.

Интерес может представлять способ обмена данными между моделями Backbone.js и средним слоем приложения через стандартный CUBA REST API. Для этого в скрипте main.js переопределена функция Backbone.sync(), и она делегирует выполнение синхронизации объекту cubaAPI, расположенному в файле cuba-api.js.

// main.js
Backbone.sync = function(method, model, options) {
   options || (options = {});
   switch (method) {
       case 'create':
           app.cubaAPI.create(model, options);
           break;
       case 'update':
           app.cubaAPI.update(model, options);
           break;
       case 'delete':
           app.cubaAPI.remove(model, options);
           break;
       case 'read':
           if (model.id)
               app.cubaAPI.load(model, options);
           else
               app.cubaAPI.loadList(model, options);
           break;
   }
};

Для работы методов объекта cubaAPI в моделях Backbone должны содержаться некоторые дополнительные поля: entityName, jpqlQuery, maxResults, view. Они используются для формирования соответствующих параметров запроса в методе loadList(). В методах update() и remove() просто формируется JSON со свойством commitInstances или removeInstances соответственно.

// cuba-api.js
(function() {
    app.cubaAPI = {
        loadList: function(collection, options) {
            var url = "api/query.json?s=" + app.session.id
                + "&e=" + collection.model.entityName + "&q=" + encodeURIComponent(collection.jpqlQuery);
            if (collection.maxResults)
                url = url + "&max=" + collection.maxResults;
            if (collection.view)
                url = url + "&view=" + collection.view;
            $.ajax({url: url, type: "GET",
                success: function(json) {
                    options.success(json);
                },
                error: function(xhr, status) {
                    options.error(xhr, status);
                }
            });
        },
        update: function(model, options) {
            var json = {
                "commitInstances": [_.clone(model.attributes)]
            };
            var url = "api/commit?s=" + app.session.id;
            $.ajax({url: url, type: "POST", contentType: "application/json", data: JSON.stringify(json),
                success: function(json, status, xhr) {
                    options.success(json);
                },
                error: function(xhr, status) {
                    options.error(xhr, status);
                }
            });
        },
        remove: function(model, options) {
            var json = {
                "removeInstances": [_.clone(model.attributes)]
            };
            var url = "api/commit?s=" + app.session.id;
            $.ajax({
            ...
            });
        },
        ...

CUBA REST API способен возвращать в JSON графы объектов с циклическими ссылками. Для этого объект, присутствующий в пути от корня более одного раза, заменяется объектом с атрибутом ref, равным идентификатору повторяющегося объекта. Для того чтобы заменить такие ref-объекты реальными объектами, в cubaAPI создан метод parse(), который переопределяет методы parse() моделей и коллекций Backbone.js.

// main.js
Backbone.Model.prototype.parse = function(resp) {
   return app.cubaAPI.parse(resp);
};
Backbone.Collection.prototype.parse = function(resp) {
   return app.cubaAPI.parse(resp);
};

Переопределена также функция initialize() модели, для того, чтобы формировать идентификаторы сущностей в виде, готовом для передачи в CUBA REST API:

// main.js
Backbone.Model.prototype.initialize = function() {
   if (!this.get("id"))
       this.set("id", this.constructor.entityName + "-" + app.guid());
};

В остальном дополнительный UI построен вполне стандартным для Backbone.js способом.

Спасибо за внимание!
В наших ближайших планах публикация статей о создании визуальных компонентов для CUBA на GWT и о разработанном в Haulmont инструменте поддержки отказоустойчивоcти серверов PostgreSQL.

Автор: krivopustov

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js