В предыдущих статьях из цикла 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 &#169;"
};

Теперь нужно объединить все компоненты и сформировать структуру интерфейса приложения. Помимо собственных компонентов сайдбар и футер, мы включаем статический 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.