СоХабр закрыт.

С 13.05.2019 изменения постов больше не отслеживаются, и новые посты не сохраняются.

H Создание десктопного приложения с помощью Webix и Electron в черновиках Из песочницы Tutorial

Статья представляет собой пошаговое описание моего опыта создания кроссплатформенного десктопного приложения с помощью Webix, Electron и Node.js.


image

Однажды мне пришла в голову светлая мысль создать десктопное приложение на базе стэка веб-технологий, который мне хорошо знаком. Знаю, что программисты, пишущие под десктоп, обычно используют 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" будут версии десктопного приложения для различных платформ.

В итоге структура проекта у меня получилась такая:
image


Корневая папка проекта должна быть расположена на сервере. В моем случае это Apache.
Итак, для начала я зашел на страницу загрузки Webix и нажал «Скачать Webix Standard». Это бесплатная версия библиотеки (лицензия «GNU GPLV3»), которая вполне подойдет для наших нужд. Имеется еще коммерческая версия «Webix PRO», которая отличается главным образом расширенной библиотекой виджетов, а также возможностями техподдержки. Из полученного архива «webix.zip» копируем папку «codebase» в корень нашего проекта. Внутри папки «codebase» обратите внимание на файлы webix.js и webix.css. Подключение этих файлов в приложении позволяет работать с Webix. В папке «skins» содержатся css-файлы с темами.

Создадим в корне проекта файл index.html.
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 содержит объект для интернационализации интерфейса приложения. По аналогии с уже имеющимися языками (русский, английский) вы можете добавить туда другие языки в случае необходимости.
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 содержатся функции, назначение которых вы можете понять из их названия и из комментариев к коду.
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. Чуть ниже я поясню его использование.

objects.js

/**
 * 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();
	    }
	}
}


Как это работает… В метод webix.ui() передается объект, имеющий многоуровневую структуру. Свойство view определяет тип виджета Webix: в нашем случае «layout». Этих типов очень много, каждый из них имеет свои методы и свойства. Кроме того, мы можем расширять стандартные компоненты Webix с помощью метода webix.protoUI(), добавляя или переопределяя необходимую нам функциональность. Как видите, работа с Webix осуществляется с помощью Javascript, поэтому весь код работы с этой библиотекой мы помещаем в теги

комментарии (34)

–1
+3 –4
vlreshet ,  
Таких постов на хабре (не с webix, правда, но тут не суть) уже с десяток. Зачем ещё один? Что принципиально нового можно узнать?
0
+2 –2
paratagas ,  
То, что именно эти инструменты можно использовать друг с другом — на примере конкретного рабочего решения.
+1
vlreshet ,  
Webix — JS библиотека. Electron использует самый обычный js движок от хрома (кое-чего нового добавлено, но стандартные апи идентичные) + HTML5, который и в африке HTML5. Следовательно, всё что работает в обычном хром браузере — всё будет работать и в електроне (но не наоборот). В чём тогда суть?
+4
+5 –1
paratagas ,   * (был изменён)
Суть в том, что в теории так оно и есть. Но на практике всегда появляются какие-то подводные камни и нюансы, на которых можно споткнуться. Например, даже такая простая вещь, как задание иконки для приложения Electron может вызвать трудности, так как в документации не указано, как одновременно задать иконку для запускаемого под Node.js приложения и скомпилированного приложения. В целом я насчитал четыре статьи про создание приложений на Electron на Хабре за последние 2 года. Не вижу ничего плохого, в том, что я поделился своим опытом работы с ним. API развивается, версии становятся более свежими, появляются новые возможности. Кроме того, возможно, кто-то просто пропустил предыдущие статьи, а из более свежих сможет узнать, что такие инструменты существуют, попробовать их и, возможно, использовать.
0
hardex ,  
Не указано, потому что компиляция — прерогатива electron-packager/electron-builder — разработок третьей стороны — а не электорна.
0
MrGobus ,  
Вы говорите как человек прочитавший и помнящий весь хабр за все года =)
В целом неплохой пост, я вот про Webix узнал (знал раньше но сейчас задумался о его пользе), да и даблы постов это неплохо если их разделяет определенный период времени. Кто-то освежит информацию, а кто-то откроет для себя что-то новое, не все же старые посты читают.
0
Zenitchik ,  
Тот, кто будет гуглить webix найдёт старые и новые посты с равной вероятностью.
0
paratagas ,  
Да, если человек знает, что искать нужно библиотеку с таким названием. Я, например, узнал о существовании Electron из комментариев к статье на Хабре про какой-то из аналогичных инструментов (точно не помню, скорее всего это NW.js).
0
Zenitchik ,  
В любом случае, если человек захочет разобраться в вопросе, он за полдня нагуглит очень многое. И среди этого многого будут посты с хабра за четыре последних года.
+3
+4 –1
max1gu ,  
скажите, сколько памяти занимает ваше трехстрочное приложение поле получаса работы?
Сам попробовал электрон, но на второй день снес — дешевле в пкстую форму приложения на дельфи7 поставить браузер на всю форму, а страницы и всё остальное зашить в ресурсы (ИЕ он и так умеет)
Дешево и сердито.
0
+1 –1
paratagas ,  
Честно говоря, не замерял. Но каких-то особых тормозов не заметил. Хотя приложение небольшое. Насчет Delphi (равно, как и других традиционных инструментов для десктопа) согласен с Вами. Но суть конкретно это приложения — больше эксперимент — освоение новых возможностей платформы Node.js. Отмечу, что в скомпилированном приложении под Win x64 один только exe-шник имеет размер около 70 Мб. Аналогичный exe-шник, сделанный, например, с помощью Windows Forms и C#, точно имел бы в разы меньший объем.
+5
paratagas ,  
Стало любопытно и я замерил (для Windows 7 x64).
Для скомпилированного приложения:
Сразу после запуска — около 26Мб, после получаса работы — около 28Мб. Разница между активным режимом (изменение данных, открытие окон и ссылок на документацию) и пассивным режимом — около 1Мб.
Для приложения, запускаемого с помощью
$ npm start
все показатели примерно на 1Мб меньше
+4
paratagas ,  
Прошу прощения — не сразу заметил все процессы приложения. Итого результат таков: основное приложение — 3 процесса примерно по 26Мб (всего около 80Мб). Плюс еще примерно по 35Мб на каждое дополнительно открытое окно Electron из меню.
0
+3 –3
herr_kaizer ,  
Жесть.
0
vSLY ,  
Спасибо за статью!
А подскажите пожалуйста, если я захочу затем портировать это приложение на мобильные устройства (желательно в виде исполняемого файла со страницей в google play/app store) какими инструментами можно воспользоваться, чтобы не менять код сильно?
Стоит задача кросс-платформенной разработки из одного исходника, пока что смотрю в сторону Delphi 10.1 Berlin, но рассматриваю и другие варианты.
0
igormatyushkin1 ,   (был удалён)
Задача сводится к тому, чтобы положить статичный веб-сайт в application bundle и отобразить его через WebView в Android, либо через WKWebView в iOS. Лучше всего создать по одному проекту для каждой платформы, используя стандартные инструменты этой самой платформы: Android Studio и Xcode для Android и iOS соответственно.
0
Leopotam ,   * (был изменён)
Если в рамках js / electron, то можно сразу смотреть в сторону reactjs, чтобы потом максимально безболезненно переехать на react-native.
0
max1gu ,  
тогда сразу на cordova
–1
paratagas ,  
Слышал о своего рода эмуляторах, например, DOSBox. Такой инструмент устанавливается на Android, после чего с его помощью можно запускать, например, exe-файлы. Сам такого рода эмуляторы не использовал, поэтому про их эффективность ничего не скажу. С другой стороны, возможно, лучше будет конвертировать для целевой платформы веб-приложение (которое в нашем случае написано на Webix), например, с помощью, PhoneGap.
+2
Goodkat ,  
Вы храните данные приложения с помощью php — у вас в скомпиллированном десктопном приложении, получается, будут браузер, вебсервер с php и node.js?
0
paratagas ,  
Да. При такой архитектуре без любого из этих компонентов (кроме браузера — он уже не нужен) скомпиллированное десктопное приложение работать не будет. В будущем планирую сделать и бэкенд и сервер на Node.js. Тогда можно будет обойтись и без отдельного веб-сервера, и без PHP.
0
Goodkat ,  
А как работает этот вебсервер? Он остаётся внутри приложения или виден извне для всех?
Что будет, если запустить три копии приложения? Они начнут конфликтовать?
Можете подробней осветить этот вопрос?
0
paratagas ,  
Сервер остается снаружи десктопа и виден извне. Если кто-то еще имеет к нему доступ (в том числе другие копии приложения), то они получат равные права на модификацию данных. Изменения данных в одной копии после обновления отражаются на других копиях. С другой стороны, если сделать бэкенд с использованием транзакций, то можно запускать несколько копий десктопного приложения на разных машинах и использовать общую базу данных.
0
Goodkat ,  
Получается, что Electron — это просто лончер для вебсервера и браузера.
И если в системе вдруг окажется другое Electron-приложение, а в списке их много, то последствия будут непредсказуемыми, они могут испортить друг другу данные.
0
paratagas ,   * (был изменён)
«Получается, что Electron — это просто лончер для вебсервера и браузера»:
В минимальном виде — да. Но Electron обладает теми же возможностями доступа, например, к ОС или файловой системе, что и Node.js. Это делает его более мощным инструментом, чем просто launcher.

«И если в системе вдруг окажется другое Electron-приложение… то… они могут испортить друг другу данные»:
Если они будут обращаться к одним и тем же данным по одному и тому же URL на сервере — то да. Но это же верно и для обычных веб-приложений при определенных условиях.

Если приложение небольшое, то, поскольку внутри Electron создается окно браузера, можно использовать localStorage. Кроме того, существуют различные инструменты и техники для доступа к БД (SQL/NoSQL), адаптированные для Electron или аналогичных ему инструментов: linvodb3, MySQL, Couchbase.
0
Goodkat ,  
Если они будут обращаться к одним и тем же данным по одному и тому же URL на сервере — то да. Но это же верно и для обычных веб-приложений при определенных условиях.
У вебприложений обычно разные серверы, а если они запущены на одном сервере, то разработчик знает о наличии других вебприложений на том же сервере.

А тут могут быть приложения от разных разработчиков, которые не зная друг о друге используют Electron и сохраняют данные используя локальный вебсервер.
Да и просто запустив такое приложение вы открываете дыру на своей машине.

Фигня какая-то получается.

Я думал, что Electron использует модифицированные вебсервер и браузер, которые контактируют как-нибудь напрямую, без открытия доступных извне сокетов.
+1
paratagas ,  
В приведенном Вами выше списке есть одно из приложений, которое требует доступ к URL
http://localhost:8000

И есть как минимум еще одно приложение, которое требует этот же URL.
В этом случае действительно возможен конфликт нескольких приложений.
Спасибо за Ваши комментарии. Поднятая Вами проблема достаточно актуальна и интересна. В свободное время попробую изучить ее детально.
–2
kostus1974 ,  
зачем это нужно? примитивный туду на 50 строк и общий размер под 50 МБ?
когда это всё кончится уже?
ну сделали вы туду на жээс — ну и что? что вам мешает просто молча запускать это просто на вашем любимом хомячном веб-сервере? зачем это публичное запихивание ноджээс во все дыры с объявлением десктопности этого? зачем это на хабре?
0
paratagas ,  
Платформа Node.js и язык Javascript развиваются. Как минимум, кому-то на Хабре будет интересно узнать про их новые возможности и существующие недостатки.
0
Zenitchik ,  
Если бы этого не было на Хабре, как бы я узнал, что это — дрянь? Самому бы шишки набивать пришлось, время тратить.
0
kostus1974 ,  
то есть слить в кучку локальный веб-сервер и браузер, и потом назвать это десктопным приложением — это надо какой-то путь пройти, как-то поумнеть? какой вы тут опыт увидели? где тут можно «шишки набивать»?
0
Zenitchik ,   * (был изменён)
слить в кучку локальный веб-сервер и браузер, и потом назвать это десктопным приложением

А почему бы нет? Не будь под капотом 50 метров мёртвой массы — всё нормально было бы.
0
justboris ,  

Насколько я понял из статьи, php вам нужен ради написания 4 строк кода.


Вот эквивалентный код на node.js. И Apache ставить не надо.


const express = require('express');
const bodyParser = require('body-parser');

const app = express();

app.use(bodyParser.json());
app.get('/data', (req, res) => {
  res.sendFile('data.json');
});
app.post('/data', (req, res) => {
   fs.writeFile('data.json', JSON.stringify(req.body), (err) => {
      if(err) {
        return res.status(500).send(`Error: ${err.message}`)
      }
      res.status(200).send(req.body);
   })
});

app.listen(3000);
0
paratagas ,  
Спасибо! Изначально я планировал более сложную систему с категориями и подкатегориями дел, думал использовать PHP-фреймворк. Но потом решил не перегружать проект, от идеи фреймворка отказался, а PHP по инерции оставил. Согласен, что использование Node.js сервера здесь намного уместнее.