Статья представляет собой пошаговое описание моего опыта создания кроссплатформенного десктопного приложения с помощью Webix, Electron и Node.js.
Однажды мне пришла в голову светлая мысль создать десктопное приложение на базе стека веб-технологий, который мне хорошо знаком. Знаю, что программисты, пишущие под десктоп, обычно используют C++, Java, C#, а на стек веб-технологий для этих целей смотрят свысока. Но, поскольку я писал приложение для себя, то справедливо решил, что использование знакомых инструментов ускорит процесс. Ну и конечно захотелось «скрестить ужа с ежом» и посмотреть что получится. Если вкратце, то получившийся результат можно запускать и как обычное веб-приложение, и как десктоп.
Код уже готового приложения можно скачать с GitHub.
Что будет делать наше приложение… Это TODO-list (а как же иначе...), в который мы сможем добавлять события, редактировать их и удалять. Событие будет иметь заголовок, содержание, место проведения, дату и приоритет. Также будет доступна возможность перевода интерфейса на русский и английский языки. Назовем его «Data master».
Для создания веб-приложения я использовал Webix. Он представляет собой кроссплатформенную и кроссбраузерную UI библиотеку, использующие компоненты для быстрого построения приложения с использованием JavaScript синтаксиса. Для компиляции веб-приложения в десктоп использовался Electron. Это кроссплатформенный инструмент, работающий на базе Node.js и позволяющий компилировать веб-приложение для запуска на различных платформах различной разрядности: Windows, Linux, Mac. Для всяких вспомогательных вещей используются инструменты на базе Node.js.
Начнем со структуры папок. В корне проекта я создал ее в таком виде:
- css — стили
- data — бэкенд
- img — изображения
- js — скрипты JavaScript
После установки модулей Node.js добавится папка «node_modules», для Webix будет использоваться папка «codebase», в папке "~/release/DataMaster" будут версии десктопного приложения для различных платформ.
Корневая папка проекта должна быть расположена на сервере. В моем случае это Apache.
Итак, для начала я зашел на страницу загрузки Webix и нажал «Скачать Webix Standard». Это бесплатная версия библиотеки (лицензия «GNU GPLV3»), которая вполне подойдет для наших нужд. Имеется еще коммерческая версия «Webix PRO», которая отличается главным образом расширенной библиотекой виджетов, а также возможностями техподдержки. Из полученного архива «webix.zip» копируем папку «codebase» в корень нашего проекта. Внутри папки «codebase» обратите внимание на файлы webix.js и webix.css. Подключение этих файлов в приложении позволяет работать с Webix. В папке «skins» содержатся css-файлы с темами.
Создадим в корне проекта файл index.html.
<!DOCTYPE HTML>
<html>
<head>
<link rel="stylesheet" href="codebase/skins/contrast.css" type="text/css">
<link rel="stylesheet" href="css/main.css" type="text/css">
<script src="codebase/webix.js" type="text/javascript"></script>
<script src="codebase/i18n/en.js" type="text/javascript"></script>
<script src="codebase/i18n/ru.js" type="text/javascript"></script>
</head>
<body>
<script src="bundle.js" type="text/javascript"></script>
</body>
</html>
Добавим webix.js. Подключение webix.css дает нам возможность использовать стандартную тему. Я же решил подключить симпатичную темненькую тему, которая лежит в «codebase/skins/contrast.css». Также мы подключили файлы из папки «codebase/i18n» для использования встроенной возможности локализации Webix. В индексного файла подключаем файл «bundle.js». Там будет находиться сборка всего нашего js-кода. Для сборки нам понадобится Node.js и Gulp.
Если у вас еще не установлена Node.js, то сделать это можно отсюда. Командами $ node -v
и $ npm -v
проверьте корректность установки Node.js и пакетного менеджера платформы — NPM.
Теперь в папке «js» мы будем создавать основную логику приложения. Файл internalization.js содержит объект для интернационализации интерфейса приложения. По аналогии с уже имеющимися языками (русский, английский) вы можете добавить туда другие языки в случае необходимости.
var translations = {
// English
"en-US": {
localeName: "en-US",
headerTitle: "Data master",
resetFilters: "Reset filters",
changeLocale: "Change language:",
loadData: "Load data",
addRow: "Add row",
clearSelection: "Clear selection",
deleteRow: "Delete row",
saveData: "Save data",
title: "Title",
noItemSelected: "No item selected",
dataSaved: "Data saved",
reservedButton: "Reserved botton"
},
// Russian
"ru-RU": {
localeName: "ru-RU",
headerTitle: "Мастер данных",
resetFilters: "Сбросить фильтры",
changeLocale: "Сменить язык:",
loadData: "Загрузить данные",
addRow: "Добавить ряд",
clearSelection: "Снять выделение",
deleteRow: "Удалить ряд",
saveData: "Сохранить",
title: "Название",
noItemSelected: "Нет выбранных рядов",
dataSaved: "Данные сохранены",
reservedButton: "Зарезервировано..."
}
};
В файле logic.js содержатся функции, назначение которых вы можете понять из их названия и из комментариев к коду.
var defaultLocale = "en-US";
// object from translations.js
var localizator = translations[defaultLocale];
/**
* Get data from backend and fill datatable grid
*/
function getData() {
$$("dataFromBackend").clearAll();
$$("dataFromBackend").load("http://localhost/data_master/data/data.php");
}
/**
* Add new row to datatable
*/
function addRow() {
$$("dataFromBackend").add(
{
title: "-----",
content: "-----",
place: "-----"
//date: "-----",
//priority: "-----"
}
);
}
/**
* Reset selection in datatable grid
*/
function clearSelection() {
$$("dataFromBackend").unselectAll();
}
/**
* Delete selected row
*/
function deleteRow() {
if (!$$("dataFromBackend").getSelectedId()) {
webix.alert(localizator.noItemSelected);
return;
}
//removes the selected item
$$("dataFromBackend").remove($$("dataFromBackend").getSelectedId());
}
/**
* Save data to backend from datatable grid
*/
function saveData() {
var grid = $$("dataFromBackend");
var serializedData = grid.serialize();
webix.ajax().post("http://localhost/data_master/data/save.php", {data: serializedData});
webix.alert(localizator.dataSaved);
}
/**
* Reset filters settings
*/
function resetFilters() {
$$("dataFromBackend").getFilter("title").value = null;
$$("dataFromBackend").getFilter("content").value = null;
$$("dataFromBackend").getFilter("place").value = null;
$$("dataFromBackend").getFilter("date").value = null;
$$("dataFromBackend").getFilter("priority").value = null;
// reload grid
$$("dataFromBackend").clearAll();
$$("dataFromBackend").load("http://localhost/data_master/data/data.php");
}
/**
* Change translation to selected
*/
function changeLocale(locale) {
localizator = translations[locale];
$$("headerContainer").define("template", localizator.headerTitle);
$$("headerContainer").refresh();
$$("resetFiltersContainer").define("value", localizator.resetFilters);
$$("resetFiltersContainer").refresh();
$$("changeLocale").define("label", localizator.changeLocale);
$$("changeLocale").refresh();
$$("loadData").define("value", localizator.loadData);
$$("loadData").refresh();
$$("addRow").define("value", localizator.addRow);
$$("addRow").refresh();
$$("clearSelection").define("value", localizator.clearSelection);
$$("clearSelection").refresh();
$$("deleteRow").define("value", localizator.deleteRow);
$$("deleteRow").refresh();
$$("saveData").define("value", localizator.saveData);
$$("saveData").refresh();
$$("reservedButton").define("value", localizator.reservedButton);
$$("reservedButton").refresh();
webix.i18n.setLocale(locale);
}
/**
* Function for reserved button
*/
function reservedButton() {
// your code...
}
Большинство функций являются обработчиками события «onclick» кнопок. Код функций в основном представляет собой способы работы с Webix-элементами. В общих чертах он интуитивно понятен, если нужна более подродная информация — добро пожаловать на страницу документации Webix.
В файле objects.js планировалось хранить функции-конструкторы, которые являются обертками над стандартными компонентами Webix. Я думал поместить туда часто используемые в приложении виджеты, но ограничился лишь одним — наиболее повторяющимся — элементом Button. Чуть ниже я поясню его использование.
/**
* Create object with type "Button"
*
* @constructor
*/
function Button(id, value, type, width, onClickFunction) {
this.view = "button";
this.id = id;
this.value = value;
this.type = type;
this.width = width;
this.on = {
"onItemClick": function(){
onClickFunction();
}
}
}
/**
* Create main layout
*/
webix.ui({
view: "layout",
id: "page",
rows:[
{
cols: [
{
view:"icon",
id: "headerIconContainer",
icon:"calendar"
},
{
view:"template",
id: "headerContainer",
type:"header",
template:"Data master"
},
new Button("resetFiltersContainer", "Reset filters", "form", 150, resetFilters),
{
id: "divider",
width: 20
},
{
view: "combo",
id: "changeLocale",
label: 'Change locale:',
labelWidth: 130,
width: 230,
align: "right",
value: "en-US",
options: [
"ru-RU",
"en-US"
],
on: {
"onChange": function(newv, oldv) {
changeLocale(newv);
}
}
}
]
},
{
view: "datatable",
id: "dataFromBackend",
columns: [
{
id: "title",
header: [
{
text: "<b>Title</b>"
},
{
content: "textFilter"
}
],
editor: "text",
fillspace: 2
},
{
id: "content",
header: [
{
text: "<b>Content</b>"
},
{
content: "textFilter"
}
],
editor: "popup",
fillspace: 8
},
{
id: "place",
header: [
{
text: "<b>Place</b>"
},
{
content: "textFilter"
}
],
editor: "text",
fillspace: 2
},
{
id: "date",
header: [
"<b>Date</b>",
{
content: "dateFilter"
}
],
editor: "date",
map: "(date)#date#",
format: webix.Date.dateToStr("%d.%m.%Y"),
fillspace: 2
},
{
id: "priority",
header: [
"<b>Priority</b>",
{
content: "selectFilter"
}
],
editor: "select",
options: [1, 2, 3, 4, 5],
fillspace: 1
}
],
editable: true,
select: "row",
multiselect: true,
// initial data load
data: webix.ajax().post("http://localhost/electron_with_backend/data/data.php")
},
{
view: "layout",
id: "buttonContainer",
height: 50,
cols: [
// Webix ui.button structure example:
/*{
view: "button",
id: "loadData",
value: "Load data",
type: "form",
width: 150,
on: {
"onItemClick": function(id, e, trg){
getData();
}
}
},*/
new Button("loadData", "Load data", "form", 150, getData),
new Button("addRow", "Add row", "form", 150, addRow),
new Button("clearSelection", "Clear selection", "form", 150, clearSelection),
new Button("deleteRow", "Delete row", "form", 150, deleteRow),
new Button("saveData", "Save data", "form", 150, saveData),
new Button("reservedButton", "Reserved button", "form", 150, reservedButton),
{}
]
}
]
});
$$("buttonContainer").define("css", "buttonContainerClass");
$$("resetFiltersContainer").define("css", "resetFiltersContainerClass");
$$("headerIconContainer").define("css", "headerIconContainerClass");
$$("headerContainer").define("css", "headerContainerClass");
$$("changeLocale").define("css", "changeLocaleClass");
$$("divider").define("css", "dividerClass");
Как это работает… В метод webix.ui() передается объект, имеющий многоуровневую структуру. Свойство view определяет тип виджета Webix: в нашем случае «layout». Этих типов очень много, каждый из них имеет свои методы и свойства. Кроме того, мы можем расширять стандартные компоненты Webix с помощью метода webix.protoUI(), добавляя или переопределяя необходимую нам функциональность. Как видите, работа с Webix осуществляется с помощью Javascript, поэтому весь код работы с этой библиотекой мы помещаем в теги <script>. В методе webix.ui() мы задали последовательность из рядов и колонок, часть которых, в свою очередь, имеют вложенные ряды и колонки, образуя сетку, параметры элементов которой мы можем задать, например, с помощью свойств «width» и «height». В колонки и ряды мы «вкладываем» элементы, настраивая их. Например, вот так можно определить кнопку:
{
view: "button",
id: "loadData",
value: "Load data",
type: "form",
width: 150,
on: {
"onItemClick": function(id, e, trg){
getData();
}
}
Свойство «id» — это свойство Webix «view_id», через которое мы можем получить доступ к элементу с помощью метода $$(). Например, $$(«loadData») вернет нам объект кнопки, описанной в коде выше. Свойство «value» определяет надпись на кнопке, «type» — тип, «width» — ширину. В объекте «on» можно задать обработчики событий для элемента. В примере выше — он один («onItemClick») и соответствует событию «onclick», которое вызывает функцию getData().
Вместо описанной выше структуры для создания элемента Button (в файле «objects.js») я использовал функцию-конструктор. Она создает и возвращает объект Button в соответствии с переданными параметрами. Это позволяет устранить дублирование кода и создавать объект таким образом:
new Button("loadData", "Load data", "form", 150, getData)
Кстати, я добавил зарезервированную кнопку для лучшего UX в скомпилированном приложении. Функциональности для нее я не придумал, поэтому можете использовать ее, как вам вздумается.
В нижней части файла components.js имеется код вида:
$$("buttonContainer").define("css", "buttonContainerClass")
Таким способом мы можем определять и изменять свойства элементов (в примере: добавление атрибута класс со значением «buttonContainerClass»). Способ, приведенный здесь, указан для наглядности. Мы можем изначально инициализировать объект каким либо классом, присвоив значение свойству «css».
Webix имеет различные способы загрузки данных в приложение и в отдельные элементы. В функции getData() я использовал метод load() для загрузки данных в грид. Метод убращается к нашему бэкенду по URL «data/data.php».
Бэкенд нашего приложения до неприличия прост. Я решил не использовать базы данных для такого маленького приложения. Данные хранятся в файле data.json, читаются оттуда с помощью data.php, и сохраняются туда же с помощью save.php.
<?php
$dataFromFile = json_decode(file_get_contents("data.json"));
echo json_encode($dataFromFile);
/*$example_json_data = array(
array (title => "My Fair Lady", year => 1964, votes => 533848, rating => 8.9, rank => 5),
array (title => "Film 1", year => 1984, votes => 933848, rating => 6.9, rank => 4),
array (title => "Film 2", year => 1966, votes => 53848, rating => 4.3, rank => 5),
array (title => "Film 3", year => 1975, votes => 567848, rating => 2.9, rank => 2),
array (title => "Film 4", year => 1981, votes => 433788, rating => 6.3, rank => 1)
);*/
//echo json_encode($example_json_data);
<?php
$data = $_POST["data"];
file_put_contents("data.json", $data);
В коммерческом проекте, конечно, следовало бы сделать различные проверки данных и обработку ошибок, но для наглядности я их опустил. В файл data-example.json я поместил образец структуры данных для загрузки в Webix элемент «datatable», взятый с сайта документации.
[
{"title":"My Fair Lady", "year":1964, "votes":533848, "rating":8.9, "rank":5},
{"title":"Film 1", "year":1984, "votes":933848, "rating":6.9, "rank":4},
{"title":"Film 2", "year":1966, "votes":53848, "rating":4.3, "rank":5},
{"title":"Film 3", "year":1975, "votes":567848, "rating":2.9, "rank":2},
{"title":"Film 4", "year":1981, "votes":433788, "rating":6.3, "rank":1}
]
Сохранение данных осуществляется в функции saveData() с помощью AJAX-метода webix.ajax().post(), которому передается URL на бэкенде и объект с данными. Вообще Webix может работать с данными по-разному, принимая и отдавая, например, json или xml. Кстати, в скачанном архиве с версией Webix, кроме папки codebase есть папка samples, в которой можно глянуть примеры работы с различными компонентами системы. В папке «samples/common/connector» есть «родная» основа для работы с бэкендом.
Таким образом, в общих чертах работа нашего приложения выполняется так… Создается сетка с рядами и колонками, в которые помещаются элементы. При взаимодействии с элементами происходят события, и выполняются обработчики, определенные для этих событий. Некоторые из обработчиков используют методы для общения с бэкендом для получения и сохранения данных. Итого мы имеем SPA-приложение, где получение и обработка данных не требуют перезагрузки страницы. Перевод интерфейса приложения осуществляется за счет взятия свойств объекта translations в соответствии с выбранной локалью, задания нового значения свойств «value» элементов и обновления этих элементов. Логика висит на событии «onChange» комбобокса и вызывает нашу функцию changeLocale(). В этой функции мы, кстати, встроенный метод webix.i18n.setLocale(locale), куда передаем локаль из комбобокса. Подробнее можно глянуть здесь.
Затем нам нужно собрать весь js код в бандл. Но сначала проделаем небольшую подготовительную работу. Создадим в корне проекта файл package.json с основными настройками приложения.
{
"name": "data_master",
"description": "Simple ToDo list with desktop building",
"version": "0.0.1",
"homepage": "https://github.com/paratagas/data_master",
"repository": {
"type": "git",
"url": "git+https://github.com/paratagas/data_master.git"
},
"author": {
"name": "Yauheni Svirydzenka",
"email": "partagas@mail.ru",
"url": "https://github.com/paratagas"
},
"tags": [
"node.js",
"webix",
"electron",
"ToDo list"
],
"main": "main.js",
"scripts": {
"start": "electron .",
"package": "electron-packager ./ DataMaster --all --out ~/release/DataMaster --overwrite"
},
"dependencies": {
"electron-prebuilt": "^0.35.6",
"electron-packager": "^8.4.0"
},
"devDependencies": {
"gulp": "^3.9.0",
"gulp-concat": "^2.6.0",
"gulp-uglify": "^1.2.0",
"gulp-sourcemaps": "^1.5.2"
},
"license": "GPL-3.0"
}
Затем выполним команду $ npm install
для загрузки необходимых компонентов. В файле gulpfile.js в корне проекта зададим настройки нашей сборки.
var gulp = require('gulp'),
uglify = require('gulp-uglify'),
concat = require('gulp-concat');
// to create source mapping
sourcemaps = require('gulp-sourcemaps');
/*
* Collect all js files to one bundle script
* Command: "gulp bundle"
*/
gulp.task('bundle', function() {
// choose any files in directories and it's subfolders
return gulp.src('js/**/*.js')
.pipe(sourcemaps.init())
.pipe(concat('bundle.js'))
.pipe(sourcemaps.write('./'))
//.pipe(uglify())
// output result to current directory
.pipe(gulp.dest('./'));
});
/*
* Watch js files changing and run task
* Command: "gulp watch"
*/
gulp.task('watch', function () {
gulp.watch('./js/**/*.js', ['bundle']);
});
Я закомментировал выполнение минификации, чтобы можно было посмотреть как в итоге выглядит bindle.js со всем нашим кодом. Кроме того, я не использовал минификацию CSS, так как у нас только один файл с небольшим количеством стилей. Вы можете изменить это поведение, если захотите. Теперь мы можем собрать проект, выполнив команду $ gulp bundle
в корне проекта. В процессе разработки команда $ gulp watch
позволяет отслеживать изменения js файлов и при наличии таковых выполнять команду $ gulp bundle
.
Наше веб-приложение уже готово и мы можем запустить его на рабочем сервере. У меня получилось что-то вроде:
Теперь давайте сделаем из него десктоп с помощью Electron. Выбрать и скачать свежую версию можно здесь. Внутри страницы каждого релиза есть список версий для различных платформ. В нашем «package.json» определены два модуля, которые позволят нам сделать основную работу. Модуль «electron-prebuilt» отвечает за предварительную сборку и запуск приложения. Отдельно модуль можно установить командой $ npm install --save-dev electron-prebuilt
. В свою очередь, модуль «electron-packager» позволяет компилировать приложения для целевой платформы или для всех возможных платформ. Отдельно устанавливается командой $ npm install --save-dev electron-packager
.
Обратите внимание на секцию:
"scripts": {
"start": "electron .",
"package": "electron-packager ./ DataMaster --all --out ~/release/DataMaster --overwrite"
},
Определив ее, вы можем запускать предсборку приложения командой $ npm start
, а компиляцию — командой $ npm run-script package
. Кстати, если мы изменим команду package, например, на "package": "electron-packager ./ DataMaster --win32-x64 --out ~/release/DataMaster --overwrite"
то приложение будет скомпилировано для целевой платформы — в нашем случае Windows x64. На данный момент Electron поддерживает платформы: Windows x32/x64, Linux x32/x64/armv7, OS X/x64. Для более полного понимания можно глянуть документацию.
Создадим в корне проекта файл main.js. Он нужен для настроек Electron.
/*
* Commands:
* npm init - initialize npm in current directory
* npm install - install modules
* npm install --save-dev electron-prebuilt - install module for pred-build
* npm install --save-dev electron-packager - install module for build
* npm start - to start app
* npm run-script package - to compile app
*/
const electron = require('electron');
// lifecycle of our app
const app = electron.app;
// create window for our app
const BrowserWindow = electron.BrowserWindow;
// To send crash reports to Electron support
// electron.crashReporter.start();
// set global link
// if not, the window will be closed after garbage collection
var mainWindow = null;
/**
* Check that all windows are closed before quiting app
*/
app.on('window-all-closed', function() {
// OS X apps are active before "Cmd + Q" command. Close app
if (process.platform != 'darwin') {
app.quit();
}
});
/**
* Create main window menu
*/
function createMenu() {
var Menu = electron.Menu;
var menuTemplate = [
{
label: 'File',
submenu: [
{
label: 'New window',
click: function() {
createSubWindow();
}
},
{type: "separator"},
{
label: 'Exit',
click: function() {
app.quit();
}
}
]
},
{
label: 'Edit',
submenu: [
{
label: 'Cut',
role: 'cut'
},
{
label: 'Copy',
role: 'copy'
},
{
label: 'Paste',
role: 'paste'
}
]
},
{
label: 'About',
submenu: [
{
label: 'Name',
click: function() {
console.log(app.getName());
}
},
{
label: 'Version',
click: function() {
console.log(app.getVersion());
}
},
{
label: 'About',
click: function() {
console.log('ToDo list');
}
}
]
},
{
label: 'Help',
submenu: [
{
label: 'Node.js docs',
click: function() {
require('electron').shell.openExternal("https://nodejs.org/api/");
}
},
{
label: 'Webix docs',
click: function() {
require('electron').shell.openExternal("http://docs.webix.com/");
}
},
{
label: 'Electron docs',
click: function() {
require('electron').shell.openExternal("http://electron.atom.io/docs/all");
}
}
]
}
];
var menuItems = Menu.buildFromTemplate(menuTemplate);
Menu.setApplicationMenu(menuItems);
}
/**
* Create main window
*/
function createMainWindow() {
mainWindow = new BrowserWindow({
title: "Data master",
resizable: false,
width: 910,
height: 800,
// set path to icon for compiled app
icon: 'resources/app/img/icon.png',
// set path to icon for launched app
//icon: 'img/icon.png'
center: true
// to open dev console: The first way
//devTools: true
});
createMenu();
// load entry point for desktop app
mainWindow.loadURL('file://' + __dirname + '/index.html');
// to open dev console: The second way
//mainWindow.webContents.openDevTools();
// Close all windows when main window is closed
mainWindow.on('closed', function() {
mainWindow = null;
newWindow = null;
});
}
/**
* Create sub menu window
*/
function createSubWindow() {
newWindow = new BrowserWindow({
title: "Go to GitHub",
resizable: false,
// imitate mobile device
width: 360,
height: 640,
icon: 'resources/app/img/mobile.png',
center: true
});
newWindow.loadURL("https://github.com/");
newWindow.on('closed', function() {
newWindow = null;
});
}
/**
* When Electron finish initialization and is ready to create browser window
*/
app.on('ready', function() {
createMainWindow();
});
В комментариях в файле описывается назначение некоторых шагов. В общих чертах мы создаем объект electron, затем окно приложения, после чего настраиваем его. После этого в окно передается основной URL приложения, например, так: mainWindow.loadURL('file://' + __dirname + '/index.html')
. В нашем случае это файл «index.html» в корне проекта. В конце выражением mainWindow = null
удаляем ссылку на окно, так как если приложение поддерживает несколько окон, то нужно ловить момент когда следует удалить соответствующий элемент. Закрытие основного окна приложения в нашем случае закрывает (присваивает null) дочернее окно. В настройках также можно задать иконку готового десктоп-приложения. Для этого указываем icon: 'resources/app/img/icon.png'
, где «resources/app» — место, где хранится исходный код в уже скомпилированном варианте приложения.
Electron также позволяет создавать кастомизированное меню окон приложения. В демонстрационных целях я добавил несколько пунктов меню, чтобы показать, как это делается. Хорошая инфа на эту тему есть вот тут и в официальной документации. В пункте меню File > New window
я добавил новое окно. Оно имитирует просмотр контента на мобильном устройстве и открывает страницу GitHub. Можно задать стартовый URL для нового окна и в нашем веб-приложении, создав таким образом еще одну точка входа, если, например, требуется обособить какой-либо функционал.
В режиме разработки можно активировать Chrome Dev Tools. В комментариях файла «main.js» указана пара способов сделать это.
Выполняем команду $ npm run-script package
и в "~/release/DataMaster" появляются готовые приложения под различные платформы.
В итоге у нас получилось вполне работоспособное приложение, которое может кому-нибудь пригодиться. Код проекта не претендует на лучшие практики разработки (хотя я и старался), но, возможно, кому-то покажутся интересными использованные технологии и их взаимодействие. Собственно, для этого я и написал эту статью. Ведь именно из таких вот статей на Хабре я в свое время узнал об этих инструментах и теперь с удовольствием их использую. Отмечу, что в приложении используется лишь небольшая часть возможностей Webix и Electron. На самом деле эти инструменты обладают довольно обширным функционалом, владение которым позволяет создавать солидные кроссплатформенные приложения.
Автор: paratagas