В предыдущих статьях из цикла “JavaScript библиотека Webix глазами новичка” вы узнали как создать приложение на основе компонентов Webix UI. В этой публикации я хочу подробно рассмотреть создание аналогичного приложения с использованием архитектуры и возможностей фреймворка Webix Jet, а также какие преимущества это дает. Статья будет полезна как для тех кто уже знаком с Webix UI, так и для новичков, ведь код библиотеки интуитивно понятен и удобен в использовании.
Обзор Webix Jet
Webix — это библиотека UI компонентов. Для создания полноценного приложения на основе Webix UI вам не обязательно использовать дополнительные инструменты. Однако по мере расширения и увеличения количества кода могут возникнуть трудности с его организацией. Команда Webix позаботилась о решении этой проблемы и разработала собственный фреймворк для построения архитектуры приложения.
Фреймворк Webix Jet позволяет создавать очень гибкое и хорошо управляемое одностраничное приложение, используя паттерн Model-View. При таком подходе, логика работы с данными и отображение элементов интерфейса четко разделяются. Каждый компонент разрабатывается и тестируется отдельно от других. Также имеются готовые решения многих задач, которые реализуются через API, плагины и конфигурации.
Архитектура Jet-приложения основана на модулях view компонентов. Каждый компонент являет собой независимую часть интерфейса. Вы можете комбинировать модули, использовать повторно в разных частях приложения и управлять их отображением с помощью URL. Подробнее о работе с Webix-jet вы узнаете на примере создания собственного SPA.
Построение собственного Webix Jet приложения
Процесс создания приложения мы рассмотрим в следующем порядке:
Интерфейс приложения
Структура приложения
Создание и конфигурация приложения
View модули интерфейса
Модели работы с данными
Локализация приложения
В документациях Webix UI и Webix Jet можно ознакомиться с использованными в статье компонентами и API фреймворка.
С кодом готового приложения можно ознакомиться тут.
Интерфейс приложения
Начнем с описания интерфейса нашего приложения и разделения его на смысловые части. Сделать это крайне важно, так как каждую обособленную часть мы будем разрабатывать и хранить в отдельном модуле.
Интерфейс разделяем на 3 части:
Тулбар
Сайдбар
Панель сменяемых модулей
Тулбар находится в верхней части экрана. Он содержит лейбл с названием приложения в левой части и кнопки переключения локализации в правой.
Сайдбар находится в левой части экрана и содержит меню для навигации между сменяемыми модулями интерфейса.
Панель сменяемых модулей составляет основу приложения и расположена в центральной части экрана. Здесь вы сможете видеть 3 автономных модуля, которые содержат определенную часть интерфейса приложения. Они будут сменять друг друга по клику на опции меню.
Структура приложения
У нашего приложения будет следующая структура:
файл index.html является стартовой страницей и единственным html файлом
файл sources/myapp.js содержит необходимые конфигурации приложения
папка sources/views состоит из следующих файлов:
top.js содержит главный модуль, который объединяет все элементы интерфейса
toolbar.js содержит модуль с описанием тулбара
form.js содержит модуль с описанием формы
films.js содержит сменяемый модуль
users.js содержит сменяемый модуль
products.js содержит сменяемый модуль
папка sources/models содержит модули для операций с данными
папка sources/styles содержит CSS файлы
папка sources/locales содержит модули локализации.
Такая организация кода помогает разделять разные аспекты разработки на отдельные составляющие и эффективно управлять ими. Это особенно актуально при работаете с большим количеством кода.
Мы будем создавать интерфейс приложения комбинируя несколько subview модулей в файле top.js. Subview - это модули интерфейса, которые входят в состав других view. Они могут быть статическими и динамическими. Давайте подробнее их рассмотрим.
Статические subview импортируются при помощи команды import и включаются в интерфейс компонента напрямую. Например, мы будем использовать статический модуль toolbar.js в главном модуле top.js.
Для понимания принципа работы динамических subview важно упомянуть о такой сущности Jet, как URL. Он позволяет формировать иерархию отображения view модулей и осуществлять навигацию между ними. URL отображается в адресной строке браузера после имени домена и спецсимвола `/#!/`.
Например, с помощью URL мы можем указать модуль, который будет отображаться первым:
http://localhost:8080/#!/top - приложение отобразит интерфейс файла top.js
http://localhost:8080/#!/top/films - приложение отобразит интерфейс файла top.js и films.js (если в top.js указан плейсхолдер для films.js).
Мы реализуем 3 динамических subview модуля, каждый из которых будет отображаться при соответствующем значении URL:
films.js ( http://localhost:8080/#!/top/films )
users.js ( http://localhost:8080/#!/top/users )
products.js ( http://localhost:8080/#!/top/products )
Навигация между динамическими subview реализуется при помощи изменения URL. Например, если нам надо отобразить модуль users на панели сменяемых модулей, мы можем изменить текущий URL на http://localhost:8080/#!/top/users и увидим интерфейс файла top.js и users.js. Переключение между опциями меню работает по такому же принципу.
Навигация с помощью URL удобна и тем, что браузер хранит историю посещения разных url-адресов, и при клике на кнопку “Назад” в браузере, мы можем перейти к предыдущему модулю. Также URL хранит состояние приложения. Если пользователь переключится на вкладку Users при помощи меню и обновит страницу, то приложение отобразит тот модуль, URL которого был установлен в адресной строке браузера до перезагрузки.
Если такое отображение URL неудобно, вы можете либо убрать спец. символы из URL, либо вовсе не показывать его в адресной строке - на работу приложения это не повлияет.
Подробнее о настройках URL можно узнать тут.
Подробнее ознакомиться с тонкостями использования subview вы можете тут.
Создание и конфигурирование приложения
Итак, мы разобрали интерфейс и структуру файлов нашего демо приложения. Сейчас пришло время непосредственно заняться его созданием. Для начала важно правильно настроить нужные конфигурации и подключить необходимые инструменты.
В файле sources/myapp.js находится модуль конфигурации приложения. Он расширяется базовым классом JetApp, который содержит все необходимые инструменты управления.
Импортируем и подключаем стили css и базовый класс JetApp.
import "./styles/app.css";
import { JetApp } from "webix-jet";
Создаем класс MyApp нашего приложения и наследуем его от базового JetApp. В конструкторе класса прописываем необходимые настройки.
Для активации режима отладки нужно установить параметр debug:true. Теперь фреймворк будет отображать все ошибки, которые могут возникнуть в процессе разработки модулей. Преимущество работы с Jet состоит в том, что сбои в работе отдельных модулей не будут влиять на работоспособность всего приложения, но без debug:true вы о них не узнаете.
Для определения стартовой страницы нужно установить параметр start и указать значение URL для начальной загрузки: start:"/top/films". Теперь при первоначальной загрузке приложения мы увидим главный модуль top.js и сменяемый модуль films.js.
export default class MyApp extends JetApp{
constructor(config){
const defaults = {
//активируем режим отладки, для отображения ошибок
debug:true,
//устанавливаем стартовый URL загрузки приложения
start:"/top/films"
};
super({ ...defaults, ...config });
}
}
После создания класса и внесения необходимых конфигураций, нам нужно его инициализировать.
const app = new MyApp();
Далее отображаем приложение через метод render() и оборачиваем его в webix.ready(), чтобы HTML страница успела загрузиться до начала выполнения кода.
webix.ready(() => app.render());
Мы создали и настроили наше приложение. Теперь самое время перейти к непосредственной разработке view модулей интерфейса.
View модули интерфейса
Мы будем создавать view компоненты как классы ES6 и наследовать их от базового JetView. Такой подход позволяет расширить модуль встроенными Jet методами и получить доступ к управлению жизненным циклом компонента.
В начале каждого модуля необходимо подключить базовый JetView класс, который хранит конфигурации и инструменты для управления логикой view модулей.
В собственных классах мы используем методы config() и init(), которые наследуются от JetView, а также можем создавать собственные методы для хранения кастомной логики.
Элементы интерфейса описываем и возвращаем через метод config().
Работу с данными осуществляем через метод init().
Модуль TopView (top.js)
Давайте создадим главный модуль TopView в файле top.js, который будет включать сайдбар, футер, тулбар и динамические subview модули. В адресной строке он будет отображаться как #!/top/ .
Тулбар мы создадим в другом модуле и импортируем сюда. Он будет включаться как статический subview.
Импортируем базовый класс JetView и модуль ToolbarView.
import { JetView } from "webix-jet";
import ToolbarView from "views/toolbar";
Создаем и наследуем класс TopView от базового JetView. В методе config() описываем основные элементы интерфейса и возвращаем комбинированный view.
export default class TopView extends JetView{
config(){
//здесь будет описан интерфейс
}
}
Обратите внимание, что созданный класс необходимо экспортировать при помощи команды export default, чтобы Webix Jet включил его в приложение.
Внутри метода config() мы описываем сайдбар, который содержит меню с опциями для переключения динамических модулей интерфейса. Для этого используем компонент List библиотеки Webix UI. В свойстве data указываем названия опций меню и их id, которые будут использоваться для формирования URL.
const sidebar = {
view:"list",
data:[
{ value:"Dashboard", id:"films" },
{ value:"Users", id:"users" },
{ value:"Products", id:"products" }
]
};
Описываем футер, который содержит информацию копирайта и находится в самом низу экрана. Мы не выделяем его в отдельный модуль, так как это незначительный элемент. Для этого используем компонент Template.
const footer = {
template:"The software is provided by <a href='https://webix.com'> webix.com </a>. All rights reserved ©"
};
Теперь нужно объединить все компоненты и сформировать структуру интерфейса приложения. Помимо собственных компонентов сайдбар и футер, мы включаем статический ToolbarView и динамические subview.
config(){
const ui = {
rows:[
ToolbarView, //включаем как статический subview модуль
{
cols:[
sidebar,
{ view:"resizer" },
{ $subview:true } //включаем динамические subview
]
},
footer
]
};
return ui;
}
Возвращаем комбинированный view ui, который включает в себя все элементы интерфейса.
Теперь нужно настроить панель сменяемых модулей (плейсхолдер { $subview:true } ).
При выборе той или иной опции меню, наше приложение будет задавать необходимый сегмент URL (films, users, products) и отображать одноименный модуль на месте плейсхолдера {$subview:true}. Обязательное условие, чтобы id опций меню и названия файлов, хранящих модули интерфейса, совпадали.
Для того чтобы установить необходимый URL, воспользуемся встроенным методом JetView this.show(id). В качестве аргумента передаем id соответствующей опции меню. Этот id задает URL относительно того модуля, в котором вызван метод show(). В нашем случае метод изменит значение URL после #!/top/ и установит туда переданный в аргументе id. Таким образом, полный URL будет #!/top/{id}.
Например, при клике на опцию Users, виджет вызовет this.show("users") и установит URL #!/top/users. На месте { $subview:true } отобразится модуль файла views/users.js .
Модуль ToolbarView (toolbar.js)
Определяем модуль ToolbarView в файле toolbar.js, который включается как статический subview в главном модуле файла top.js.
Создаем и наследуем класс ToolbarView от базового JetView. В методе config() описываем необходимые компоненты и возвращаем их.
Описываем тулбар, который содержит лейбл с названием приложения и кнопки переключения локализации. Для этого используем такие компоненты Webix UI как Toolbar, Label и Segmented.
export default class TopView extends JetView{
config(){
const toolbar = {
view:"toolbar",
elements:[
{ view:"label", label:"Demo App" },
{
view:"segmented",
options:[
{ id:"en", value:"En" },
{ id:"ru", value:"Ru" }
]
}
]
};
return toolbar;
}
}
Теперь пришло время перейти к созданию динамических subview, которые будут сменяться в зависимости от сегмента URL, следующего после #!/top/. У нас будет 3 сменяемых модуля:
FilmsView
UsersView
ProductsView.
Каждый модуль мы будем разрабатывать в отдельном файле.
Модуль FilmsView (films.js)
Интерфейс модуля состоит из таблицы фильмов и формы для их редактирования. Таблица состоит из 5 столбцов, в которых мы реализуем сортировку и фильтрацию данных. Форму создадим в отдельном модуле и будем включать ее как статический subview.
Определяем модуль FilmsView в файле films.js, который будет включаться как динамический subview в файле top.js. Модуль будет отображаться при значении URL: #!/top/films.
В начале кода импортируем модуль FormView из файла form.js, который содержит форму для редактирования фильмов и будет включаться как статический subview.
import FormView from "views/form";
Создаем и наследуем класс FilmsView от базового JetView. В методе config() описываем основные элементы интерфейса и возвращаем комбинированный view.
Описываем таблицу фильмов с помощью компонента Datatable. Через свойство columns конфигурируем столбцы. У каждого столбца должен быть уникальный id. Сортировка данных столбца реализуется при помощи свойства sort. Для столбцов с текстовыми значениями мы используем sort:"text", а для числовых sort:"int". Чтобы добавить фильтр в хедер столбца, мы используем конфигурацию {content:"textFilter"} для текстовых значений и {content:"selectFilter"} для числовых.
const film_table = {
view:"datatable",
columns:[
{ id:"id", header:""},
{ id:"title", header:["Film title", { content:"textFilter" }], fillspace:true, sort:"text" },
{ id:"year", header:["Released", { content:"selectFilter" }], sort:"int" },
{ id:"votes", header:"Votes", sort:"int" },
{ id:"rating", header:"Rating", sort:"int" }
]
};
Формируем интерфейс модуля. Включаем собственный компонент film_table с таблицей фильмов и статический модуль FormView, который хранит интерфейс формы для редактирования фильмов.
return {
cols:[
film_table,
FormView //включаем как статический subview модуль
]
};
Модуль FormView (form.js)
Интерфейс модуля содержит форму для редактирования фильмов. Форма состоит из 4 полей ввода, а также кнопок “Сохранить” и “Очистить”.
Определяем модуль FormView в файле form.js. Создаем и наследуем класс FormView от базового JetView. В методе config() описываем основные элементы интерфейса и возвращаем комбинированный view.
Описываем форму для редактирования фильмов с помощью компонента Form. Через свойство elements конфигурируем поля ввода и кнопки для управления формой.
config(){
const film_form = {
view:"form",
elements:[
{ type:"section", template:"edit films" },
{ view:"text", name:"title", label:"Title" },
{ view:"text", name:"year", label:"Year" },
{ view:"text", name:"rating", label:"Rating" },
{ view:"text", name:"votes", label:"Votes" },
{
cols:[
{ view:"button", value:"Save", css:"webix_primary" },
{ view:"button", value:"Clear", css:"webix_secondary" }
]
},
{}
]
};
return film_form;
}
Модуль UsersView (users.js)
Интерфейс модуля состоит из тулбара, списка пользователей и диаграммы пользователей. В тулбаре находится строка поиска, кнопка для добавления новых пользователей, а также 2 кнопки для сортировки списка пользователей по возрастанию и убыванию. Список пользователей содержит имя, возраст и страну. Диаграмма отображает возраст пользователей.
Определяем модуль UsersView в файле users.js, который будет включаться как динамический subview в файле top.js. Модуль будет отображаться при значении URL: #!/top/users.
Создаем и наследуем класс UsersView от базового JetView. В методе config() описываем элементы интерфейса при помощи компонентов List, Toolbar и Chart.
Описываем список пользователей при помощи компонента List. Через свойство template конфигурируем шаблон списка, который будет включать имя, возраст и страну.
const list = {
view:"list",
template:"#name#, #age#, #country#"
};
Описываем тулбар для работы со списком при помощи компонента Toolbar . Через свойство elements конфигурируем строку поиска и кнопки для управления списком пользователей.
const list_toolbar = {
view:"toolbar",
elements:[
//кнопка добавления нового пользователя
{ view:"button", value:"Add new person", css:"webix_primary" },
//строка поиска
{ view:"search" },
//кнопки сортировки
{ view:"button", value:"Sort asc" },
{ view:"button", value:"Sort desc" }
]
};
Описываем диаграмму пользователей при помощи компонента Chart. Задаем тип диаграммы type:"bar", значения для столбцов value:"#age#". Конфигурируем оси при помощи свойств xAxis и yAxis.
const chart = {
view:"chart",
type:"bar",
value:"#age#",
xAxis:{
template:"#name#",
title:"Age"
},
yAxis:{
start:0,
end:100,
step:10
}
};
Формируем интерфейс модуля. Комбинируем и возвращаем компоненты list_toolbar, list и chart.
return { rows:[list_toolbar, list, chart] };
Модуль ProductsView (products.js)
Интерфейс модуля состоит из древовидной таблицы, которая содержит элементы высшего и вложенного уровней.
Определяем модуль ProductsView в файле products.js, который будет включаться как динамический subview в файле top.js. Модуль будет отображаться при значении URL: #!/top/products.
Создаем и наследуем класс ProductsView от базового JetView. В методе config() описываем древовидную таблицу при помощи компонента Treetable. Через свойство columns конфигурируем столбцы таблицы. Используем конфигурацию template:"{common.treetable()} #title#", чтобы задать вложенные элементы.
config(){
const products_treetable = {
view:"treetable"
columns:[
{ id:"id", header:"" },
{ id:"title", header:"Title", fillspace:true, template:"{common.treetable()} #title#" },
{ id:"price", header:"Price" }
]
};
return products_treetable;
}
Мы описали интерфейс нашего приложения. Теперь пришла очередь рассказать о том, как осуществляется работа с данными.
Модели работы с данными
Давайте разбираться с тем, как работать с моделями данных. В принципе, подключать данные не обязательно, и можно загружать их сразу в компоненты минуя модели. Но для серьезного приложения такие сущности как view и model лучше разделять.
Логика работы с моделями данных хранится в файлах директории sources/models. Предполагается, что мы взаимодействуем с сервером и загружаем данные асинхронно. Для этого используем метод webix.ajax().
Функция webix.ajax() делает асинхронный запрос по указанному в аргументе адресу и возвращает промис-объект. В случае успешной загрузки мы получим объект, значения которого будем загружать в необходимые нам модули.
Данные нам нужны для 3 сменяемых subview:
FilmsView
UsersView
ProductsView.
Для каждого модуля создаем отдельную модель, как на примере ниже.
// models/films.js
export function getData(){
return webix.ajax("../../data/film_data.js");
}
Все 3 модели будут отличаться только аргументом запроса, в котором мы указываем путь к данным на сервере. В нашем случае данные хранятся в папке ../../data/.
Теперь для каждого view модуля необходимо импортировать метод getData из соответствующей модели.
import { getData } from "models/films"; // для FilmsView
import { getData } from "models/users"; //для UsersView
import { getData } from "models/products"; //для ProductsView.
Далее мы должны передать данные из модели в необходимый компонент. Интерфейс модуля описывается внутри метода config() и становится доступным после инициализации. Внутри специального метода init() класса JetView, мы можем получить доступ к нужному data-компоненту (List, Datatable, Chart) и загрузить в него данные при помощи метода parse().
config(){
...
return {
cols:[
film_table,
FormView
]
};
}
init(view){
const datatable = view.queryView("datatable");
datatable.parse(getData());
}
В метод init() приходит параметр view, который ссылается на объект, возвращенный методом config() того же модуля. Это может быть layout с таблицей и другими компонентами. Внутри layout мы ищем таблицу по названию компонента (view.queryView("datatable");), а затем загружаем в нее данные.
Чтобы передать данные из промис-объекта который возвращает нам метод getData(), нужно вызвать метод parse() для Webix компонента и передать туда промис в качестве аргумента.
Когда данные будут загружены, компонент их обработает и отобразит в интерфейсе приложения.
Локализация приложения
Инструментарий фреймворка Webix Jet позволяет с легкостью реализовать локализацию приложения. Для этого мы будем использовать встроенный сервис Locale.
Этот сервис нужно подключить в файле sources/myapp.js, т.к. он будет использоваться глобально для всего приложения. Для этого мы импортируем модуль plugins, который содержит встроенные сервисы, вместе с базовым JetApp.
import { JetApp, plugins } from "webix-jet";
После создания и инициализации класса MyApp, мы подключаем сервис plugins.Locale. Сделать это необходимо до вызова метода render().
const app = new MyApp();
app.use(plugins.Locale);
webix.ready(() => app.render());
Сервис Locale устанавливает связь с файлами локализации, которые находятся в директории sources/locales. Файлы en.js и ru.js хранят объекты с элементами локализации.
// locales/en.js
export default {
Dashboard : "Dashboard",
Users : "Users",
...
}
// locales/ru.js
export default {
Dashboard : "Панель",
Users : "Пользователи",
...
}
Далее, в каждом отдельном view модуле мы получаем метод из сервиса локализации.
const _ = this.app.getService("locale")._;
Этот метод мы будем вызывать каждый раз, когда нам надо перевести какое-либо текстовое выражение. В качестве аргумента нужно передавать ключи, по которым будут подтягиваться значения из объекта файла установленной локали: _("some text"). При смене локализации, Jet перерисует приложение с учетом значений установленной локали.
Почти готово, осталось только рассказать о том, каким образом пользователь сможет переключать языки в нашем приложении. Как вы помните, в тулбаре мы создали кнопку Segmented со значениями En и Ru. Теперь нам нужно создать обработчик, который будет вызываться при клике на соответствующую кнопку и применять нужную локализацию.
config(){
...
{
view:"segmented",
options:[
{ id:"en", value:"En" },
{ id:"ru", value:"Ru" }
],
click:() => this.toggleLanguage()
}
...
}
toggleLanguage(){
const langs = this.app.getService("locale");
const value = this.getRoot().queryView("segmented").getValue();
langs.setLang(value);
}
Для этого мы создаем функцию-обработчик toggleLanguage() как метод класса ToolbarView. Здесь же находится сегментная кнопка, при клике на которую мы будем вызывать наш обработчик.
Внутри функции получаем доступ к сервису локализации, считываем значение заданной опции (значение берется из id - "en" или "ru") и устанавливаем выбранную локаль с помощью метода setLang().
При описании функции используем метод this.getRoot(). С его помощью мы получаем доступ к view, возвращенный нам методом config() - здесь это Toolbar. Внутри view мы ищем компонент с именем segmented, чтобы получить объект кнопки и считать ее значение.
Вот такими простыми манипуляциями в Webix Jet реализуется смена локализации.
Заключение
С помощью фреймворка Webix Jet мы создали структурированное одностраничное приложение. Интерфейс разделен на модули view, которые хранятся в отдельной директории sources/views. Логика работы с данными отделена от компонентов интерфейса и хранится в директории sources/models. Реализована локализация приложения за счет использования плагина Locale базового класса JetApp.
Сейчас это мини-приложение, но с такой архитектурой его легко масштабировать и поддерживать.
С кодом готового приложения можно ознакомиться тут.
Webix Jet имеет достаточно широкий спектр инструментов и конфигураций, которые значительно упрощают процесс разработки приложения на основе компонентов Webix UI. Более детально мы рассмотрим их в следующих статьях посвященных Webix Jet.