Принципы модульной разработки в bpm'online
Glossary Item Box
Виды модулей bpm'online
Всю клиентскую функциональность bpm'online условно можно разделить на следующие группы:
- Базовые библиотеки
- Ядро
- Песочница
- Клиентские модули
Базовые библиотеки
Базовые библиотеки — это сторонние JavaScript-библиотеки, которые используются в приложении. В качестве загрузчика модулей используется библиотека RequireJS. В конфигурационной логике используется функциональность фреймворка ExtJS для работы с элементами управления интерфейса. Также используются такие фреймворки, как JQuery, Backbone, SyncFusion и др. Все сторонние JavaScript-библиотеки помещены в каталог Terrasoft.WebApp\Resources\ui приложения.
Ядро
Основная задача ядра клиентской части bpm'online — предоставление унифицированного интерфейса для взаимодействия всех остальных клиентских частей системы. Так, ядро предоставляет API для доступа к базовым клиентским библиотекам, определяет содержимое песочницы для модулей, предоставляет доступ к системным перечислениям и константам, реализует клиентский механизм работы с данными и т.д. При этом ядро не работает напрямую с модулями системы, а знает только о существовании основного модуля приложения — ViewModule, в который выполняет загрузку всех остальных модулей.
Для того чтобы получить доступ к функциям ядра, используемым в клиентской пользовательской логике, модуль должен импортировать в качестве зависимости модуль terrasoft.
Песочница
Модуль в системе является изолированной программной единицей. Ему ничего не известно об остальных модулях системы, кроме списка имен модулей, от которых он сам зависит. Для организации взаимодействия таких разрозненных модулей между собой предназначен специальный объект — песочница (sandbox).
Песочница предоставляет два ключевых механизма взаимодействия модулей в системе:
- Механизм обмена сообщениями между модулями. Модули могут общаться друг с другом только посредством сообщений. Модуль, который хочет сообщить об изменении своего состояния другим модулям в системе, публикует сообщение, используя метод песочницы sandbox.publish(). Если модуль хочет получать сообщения об изменении состояний других модулей, он должен подписаться на них. Подписка выполняется через вызов метода песочницы sandbox.subscribe().
- Загрузка модулей по требованию в заданную область отображения (для визуальных модулей). В процессе решения задач пользовательской бизнес-логики может возникнуть необходимость динамической (во время выполнения приложения) загрузки некоторых модулей, не объявленных как зависимости. Загрузку этих модулей можно выполнить в коде объявления модуля, в функции define(). Для этого используется метод песочницы sandbox.load().
Для того чтобы модуль мог взаимодействовать с другими модулями системы через песочницу, он должен импортировать в качестве зависимости модуль sandbox.
Клиентские модули
Клиентские модули — это отдельные блоки функциональности, которые загружаются и запускаются по требованию в соответствии с технологией AMD. Реализация всей пользовательской функциональности выполняется в клиентских модулях. Несмотря на некоторые функциональные различия, все клиентские модули bpm'online имеют одинаковую структуру описания, которая соответствует формату описания модулей AMD. Подробнее о клиентских модулях можно узнать из статьи "Клиентские модули".
Модули ext-base, terrasoft и sandbox
Bpm'online содержит модули, которые используются в большинстве клиентских модулей конфигурации. Это модуль ext-base функциональности фреймворка ExtJs, модуль terrasoft пространства имен и объектов Terrasoft и модуль sandbox так называемой песочницы, которая реализует механизм обмена сообщениями между модулями. Доступ к этим модулям может быть получен следующим образом:
// Определение модуля и получение ссылок на модули-зависимости. define("ExampleModule", ["ext-base", "terrasoft", "sandbox"], // Ext — ссылка на объект, дающий доступ к возможностям фреймворка ExtJs. // Terrasoft — ссылка на объект, дающий доступ к системным переменным, переменным ядра и т.д. // sanbox — используется для обмена сообщениями между модулями. function (Ext, Terrasoft, sandbox) { });
Указывать базовые модули в зависимостях ["ext-base", "terrasoft", "sandbox"] не обязательно. Объекты Ext, Terrasoft и sanbox после создания объекта класса модуля будут доступны как свойства объекта: this.Ext, this.Terrasoft, this.sanbox.
Объявление класса модуля. Метод Ext.define()
Одна из важных функций javascript-фреймворка ExtJs в bpm'online — это обьявление классов. Для этого используется стандартный механизм библиотеки — метод define() глобального объекта Ext. Пример объявления класса с помощью этого метода:
// Terrasoft.configuration.ExampleClass — название класса с // соблюдением пространства имен. Ext.define("Terrasoft.configuration.ExampleClass", { // Сокращенное название класа. alternateClassName: "Terrasoft.ExampleClass", // Название класса, от которого происходит наследование. extend: "Ext.String", // Блок для объявления статических свойств и методов. static: { // Пример статического свойства. myStaticProperty: true, // Пример статического метода. getMyStaticProperty: function () { // Пример доступа к статическому свойству. return Terrasoft.ExampleClass.myStaticProperty; } }, // Пример динамического свойства. myProperty: 12, // Пример динамического метода класса. getMyProperty: function () { return this.myProperty; } });
Примеры различных вариантов создания экземпляров класса:
// Создание экземпляра класса по полному имени. var exampleObject = Ext.create("Terrasoft.configuration.ExampleClass"); // Создание экземпляра класса по сокращенному названию — псевдониму. var exampleObject = Ext.create("Terrasoft.ExampleClass"); // Создание экземпляра класса с указанными свойствами. var exampleObject = Ext.create("Terrasoft.ExampleClass", { // Переопределение значения свойства объекта с 12 на 20. myProperty: 20, // Определение нового метода для текущего экземпляра класса. myNewMethod: function () { return this.getMyProperty() * 2; } });
Наследование класса модуля
В самой простой реализации модуля его содержимое является или простым объектом c набором методов и свойств, или функцией-конструктором, которые модуль должен возвращать в функции, которая вызывается после его загрузки.
define("ModuleExample", [], function () { // Пример модуля, возвращающего простой объект. return { init: function () { // Метод будет вызван при инициализации модуля, // но содержимое модуля не попадет в DOM. } } }); define("ModuleExample", [], function () { // Пример модуля, возвращающего функцию-конструктор. return function () { this.init = function () { // Метод будет вызван при инициализации модуля, // но содержимое модуля не попадет в DOM. } } });
Такой простой модуль не сможет добавить свое представление в Document Object Model (DOM), если явно не реализовать метод render(), который вернет экземпляр представления и вставит его в DOM. Логика вызова метода render() у объекта модуля описана на уровне ядра приложения. Также в таком модуле не реализован метод destroy(). Если модуль визуальный, т.е. содержит метод render(), то выгрузить представление из DOM, не реализовав логику выгрузки в методе destroy(), не удастся, и представление модуля будет оставаться в DOM.
В большинстве случаев класс модуля правильно наследовать от Terrasoft.configuration.BaseModule или Terrasoft.configuration.BaseSchemaModule, в которых в необходимой степени уже реализованы следующие методы:
- init() — метод инициализации модуля. Отвечает за инициализацию свойств объекта класса, а также за подписку на сообщения.
- render() — метод отрисовки представления модуля в DOM. Должен возвращать представление. Принимает единственный агрумент renderTo — элемент, в который будет вставлено представление объекта модуля.
- destroy() — метод, отвечающий за удаление представления модуля, удаление модели представления, отписку от ранее подписанных сообщений и уничтожение объекта класса модуля.
Ниже приведен пример простого класса модуля, наследуемого от "Terrasoft.BaseModule". Данный модуль добавляет кнопку в DOM. При клике на кнопку выводится сообщение, после чего она удаляется из DOM.
define("ModuleExample", [], function () { Ext.define("Terrasoft.configuration.ModuleExample", { // Короткое название класса. alternateClassName: "Terrasoft.ModuleExample", // Класс, от которого происходит наследование. extend: "Terrasoft.BaseModule", // Обязательное свойство. Если не определено, будет сгенерирована ошибка на уровне // "Terrasoft.core.BaseObject", так как класс наследуется от "Terrasoft.BaseModule". Ext: null, // Обязательное свойство. Если не определено, будет сгенерирована ошибка на уровне // "Terrasoft.core.BaseObject", так как класс наследуется от "Terrasoft.BaseModule". sandbox: null, // Обязательное свойство. Если не определено, будет сгенерирована ошибка на уровне // "Terrasoft.core.BaseObject", так как класс наследуется от "Terrasoft.BaseModule". Terrasoft: null, // Модель представления. viewModel: null, // Представление. В качестве примера используется кнопка. view: null, // Если не реализовать метод init() в этом классе, // то при создании экземпляра текущего класса будет вызван метод // init() класс-родителя Terrasoft.BaseModule. init: function () { // Вызывает выполнение логики метода init() класса-родителя. this.callParent(arguments); this.initViewModel(); }, // Инициализирует модель представления. initViewModel: function () { // Сохранение контекста класса модуля // для доступа к нему из модели представления. var self = this; // Создание модели представления. this.viewModel = Ext.create("Terrasoft.BaseViewModel", { values: { // Заголовок кнопки. captionBtn: "Click Me" }, methods: { // Обработчик нажатия на кнопку. onClickBtn: function () { var captionBtn = this.get("captionBtn"); alert(captionBtn + " button was pressed"); // Вызывает метод выгрузки представления и модели представления, // что приводит к удалению кнопки из DOM. self.destroy(); } } }); }, // Создает представление (кнопку), // связывает ее с моделью представления и вставляет в DOM. render: function (renderTo) { // В качестве представления создается кнопка. this.view = this.Ext.create("Terrasoft.Button", { // Контейнер, в который будет помещена кнопка. renderTo: renderTo, // HTML-атрибут id. id: "example-btn", // Название класса. className: "Terrasoft.Button", // Заголовок. caption: { // Связывает заголовок кнопки // со свойством captionBtn модели представления. bindTo: "captionBtn" }, // Метод-обработчик события нажатия на кнопку. click: { // Связывает обработчик события нажатия на кнопку // с методом onClickBtn() модели представления. bindTo: "onClickBtn" }, // Стиль кнопки. Возможные стили определены в перечислении // Terrasoft.controls.ButtonEnums.style. style: this.Terrasoft.controls.ButtonEnums.style.GREEN }); // Связывает представление и модель представления. this.view.bind(this.viewModel); // Возвращает представление, которое будет вставлено в DOM. return this.view; }, // Удаляет неиспользуемые объекты. destroy: function () { // Уничтожает представление, что приводит к удалению кнопки из DOM. this.view.destroy(); // Удаляет неиспользуемую модель представления. this.viewModel.destroy(); } }); // Возвращает объект модуля. return Terrasoft.ModuleExample; });
Синхронная и асинхронная инициализация модуля
Существует два способа инициализации экземпляра класса модуля — синхронная и асинхронная инициализация.
Синхронная инициализация
Синхронно модуль инициализируется, если при его загрузке явно не указано свойство isAsync: true конфигурационного объекта, передаваемого в качестве параметра метода loadModule(). Например, если выполнить:
this.sandbox.loadModule([moduleName])
то методы класса модуля будут загружены синхронно. Первым будет вызван метод init(), после которого сразу же будет выполнен метод render().
Пример реализации синхронно инициализируемого модуля.
define("ModuleExample", [], function () { Ext.define("Terrasoft.configuration.ModuleExample", { alternateClassName: "Terrasoft.ModuleExample", Ext: null, sandbox: null, Terrasoft: null, init: function () { // При инициализации выполнится первым. }, render: function (renderTo) { // При инициализации модуля выполнится сразу же после метода init. } }); });
Асинхронная инициализация
Асинхронно модуль инициализируется, если при его загрузке явно указать свойство isAsync: true конфигурационного объекта, передаваемого в качестве параметра метода loadModule(). Например, если выполнить:
this.sandbox.loadModule([moduleName], { isAsync: true })
При таком подходе в метод init() будет передан единственный параметр — callback-функция с контекстом текущего модуля. При вызове callback-функции будет вызван метод render() загружаемого модуля. Представление будет добавлено в DOM только после выполнения метода render().
Пример реализации асинхронно инициализируемого модуля.
define("ModuleExample", [], function () { Ext.define("Terrasoft.configuration.ModuleExample", { alternateClassName: "Terrasoft.ModuleExample", Ext: null, sandbox: null, Terrasoft: null, // При инициализации модуля выполнится первым. init: function (callback) { setTimeout(callback, 2000); }, render: function (renderTo) { // Метод выполнится c задержкой в 2 секудны, // Задержка указана в аргументе функции setTimeout() в методе init(). } }); });
Цепочки модулей
Иногда возникает необходимость показать представление некой модели на месте представления другой модели. Например, для установки значения определенного поля на текущей странице нужно показать страницу SelectData для выбора значения из справочника. В таких случаях нужно, чтобы не выгружался модуль текущей страницы, а на месте его контейнера отображалось представление модуля страницы выбора из справочника. Для этого можно использовать цепочки модулей.
Для того чтобы начать построение цепочки, достаточно добавить свойство keepAlive в конфигурационный объект загружаемого модуля. Например, в модуле текущей страницы CardModule необходимо вызвать модуль выбора из справочника selectDataModule. Для этого нужно выполнить следующий код:
sandbox.loadModule("selectDataModule", { // Id представления загружаемого модуля. id: "selectDataModule_id", // Представление будет добавлено в контейнер текущей страницы. renderTo: "cardModuleContainer", // Указывает, чтобы текущий модуль не выгружался. keepAlive: true });
После выполнения кода построится цепочка модулей из модуля текущей страницы и модуля страницы выбора из справочника. Из модуля текущей страницы selectData, нажав на кнопку [Добавить новую запись], можно открыть новую страницу, добавив в цепочку еще один элемент. Таким образом в цепочку модулей можно добавлять сколько угодно экземпляров модулей. Активный модуль (тот, который отображен на странице) — всегда последний элемент цепочки. Если установить активным элемент из середины цепочки, то все элементы, находящиеся в цепочке после него, будут уничтожены. Для того чтобы активировать элемент цепочки, достаточно вызвать функцию loadModule, в параметр которой передать идентификатор модуля:
sandbox.loadModule("someModule", { id: "someModuleId" });
Ядро уничтожит все элементы цепочки после указанного и выполнит стандартную логику при загрузке модуля — вызовет методы init() и render(). При этом в метод render() будет передан контейнер, в который был помещен предыдущий активный модуль. Все модули цепочки могут работать как и раньше — принимать и отправлять сообщения, сохранять данные и т.п.
Если при вызове метода loadModule() в конфигурационный объект не добавлять свойство keepAlive или добавить его со значением keepAlive: false, то цепочка модулей будет уничтожена.