Введение
Доброе время суток! Хотелось поделиться с Вами личным опытом создания десктопного приложения на JavaScript с использованием связки Electron и Webix. Такая связка позволяет ускорить процесс верстки интерфейса, особо не тратя время на разметку и прочие web штуки, которыми может заняться как раз фреймворк Webix.
Инструменты
Итак приступим, на понадобится следующие инструменты:
- Редактор, в котором мы будем писать непосредственно наш код. Я буду использовать visual studio code (VSC), который можно взять отсюда ;
- Сервер Node.js, который можно взять отсюда . Скачиваем его и устанавливаем;
- Фреймворк «Webix» бесплатную версию (Webix Standard is a free UI library under GNU GPLv3 license), которую берем вот отсюда webix.com/get-webix-gpl. Для того что бы его скачать нужно перейти по выше приведенной ссылке, вести email, имя и фамилию, поставить три галочки нажать отправить после чего Вам на почту отправят ссылку для скачивания.
В общем то вот и все, что нам с Вами потребуется.
Устанавливаем необходимые инструменты
Итак, переходим непосредственно к созданию:
1. Устанавливаем «node.js». Тут я останавливаться не буду я уверен с этим Вы справитесь без меня.
2. Создаем папку, в которой будем писать наш код — например electron_webix (рис. 1).
Рис. 1 – Создание рабочей папки
3. Запускаем VSC и открываем эту папку (рис. 2).
Рис. 2 – Открываем рабочую папку
4. Открываем в VSC терминал комбинацией клавиш «Ctr+`» и вводим команду «npm init», которая произведет инициализацию нашего «node.js» проекта. После чего система запросит кучу разных и очень полезных вопросов, на которые отвечать не обязательно. Далее жмем все время уверенно на кнопку «Enter» и не думаем не о чем плохом (рис. 3).
Рис. 3 – Инициализация проекта
5. Устанавливаем непосредственно сам «Electron». Для чего в консоли VSC вводим команду «npm install --save-dev electron», после сидим и ждем пока все установиться (рис. 4).
Рис. 4 – Установка «Electron»
Организация рабочего пространства
Теперь переходим к организации рабочее пространство. Первое что мы должны сделать это создать несколько папок и файлов, без которых не обойтись (рис. 5):
- папку «libs», сюда положим файлы скаченные с сайта «Webix»;
- папку «.vscode», сюда положим json файл, который будет запускать наше приложение в VSC;
- файл «main.js», основной файл, который будет запускать наше приложения «Electron»;
- файл «index.html», будет содержать наш контент и разметку;
- файл «renderer.js», обработчик событий нашего приложения. Работает в связке с index.html;
- файл «launch.json», в папке «.vscode» файл который нужен для запуска нашего кода в среде «VSC» кнопочкой «F5»;
- файл «style.css», в этом файле нам все таки придется прописать кое-какие стили что бы наше окошко выглядело по приличней.
Рис. 5 – Необходимые папки и файлы
Наполняем рабочее пространство первоначальным содержимым
Начнем наполнение рабочего пространства с файла «launch.json», который используется для запуска нашего приложения в среде «VSC» без всяких дополнительных команд из консоли.
Содержимое файла «launch.json» было взято вот отсюда. Тут мы не будем вникать в содержимое данного файла просто его скопируем и сохраним. Содержимое файла представлено ниже (рис. 6).
"version": "0.2.0",
"configurations": [
{
"name": "Debug Main Process",
"type": "node",
"request": "launch",
"cwd": "${workspaceFolder}",
"runtimeExecutable": "${workspaceFolder}/node_modules/.bin/electron",
"windows": {
"runtimeExecutable": "${workspaceFolder}/node_modules/.bin/electron.cmd"
},
"args" : ["."],
"outputCapture": "std"
}
]
}
Рис. 6 – Файл «launch.json» в среде «VSC»
Далее нам необходимо подправить немного файл «package.json», который был автоматически создан при выполнении команды «npm init». В нем нужно произвести изменение в двух строках как показано ниже (рис. 7):
Был вот такой «package.json»:
"name": "electron_webix",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1
},
"author": "",
"license": "ISC",
"devDependencies": {
"electron": "^8.2.3"
}
}
Стал вот такой «package.json»:
{
"name": "electron_webix",
"version": "1.0.0",
"description": "",
"main": "main.js",
"scripts": {
"start": "electron ."
},
"author": "",
"license": "ISC",
"devDependencies": {
"electron": "^8.2.3"
}
}
Рис. 7 – Файл «package.json» в среде «VSC»
Теперь переходим к наполнению файла «main.js», его содержимое копируем вот отсюда. Вставляем и сохраняем.
const { app, BrowserWindow } = require('electron')
function createWindow () {
// Создаем окно браузера.
const win = new BrowserWindow({
width: 800,
height: 600,
webPreferences: {
nodeIntegration: true
}
})
// and load the index.html of the app.
win.loadFile('index.html')
// Отображаем средства разработчика.
win.webContents.openDevTools()
}
// This method will be called when Electron has finished
// initialization and is ready to create browser windows.
// Некоторые API могут использоваться только после возникновения этого события.
app.whenReady().then(createWindow)
// Quit when all windows are closed.
app.on('window-all-closed', () => {
// Для приложений и строки меню в macOS является обычным делом оставаться
// активными до тех пор, пока пользователь не выйдет окончательно используя Cmd + Q
if (process.platform !== 'darwin') {
app.quit()
}
})
app.on('activate', () => {
// На MacOS обычно пересоздают окно в приложении,
// после того, как на иконку в доке нажали и других открытых окон нету.
if (BrowserWindow.getAllWindows().length === 0) {
createWindow()
}
})
// In this file you can include the rest of your app's specific main process
// code. Можно также поместить их в отдельные файлы и применить к ним require.
Рис. 8 – Файл «main.js» в среде «VSC»
После чего мы уже можем наконец-то выполнить первый запуск «клавиша F5» нашего приложения. Итак, жмем «F5» и вот оно наше окошко (рис. 9).
Рис. 9 – Файл «main.js» в среде «VSC»
Что мы видим на рис. 9? Это слева рабочая область нашего приложения, а справа средства отладки приложения.
Теперь создадим первоначальную разметку в файле «index.html». Для чего в «VSC» делаем следующие: открываем файл «index.html»=> вводим «!» => и жмем на «Enter»=> а дальше магия и следующий результат:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
</body>
</html>
Ну и как в лучших традициях написания кода между тегами «» вставим текст «Hello world!!!»
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
Hello world!!!
</body>
</html>
После чего пробуем нажать опять «F5» и проверяем, что у нас все работает (рис. 10)
Рис. 10 – Окно «Hello world!!!»
Теперь нужно из скаченного нами архива webix.zip вытащить все содержимое папки «codebase» и перенести его в нашу папку libs (рис. 11).
Открываем архив заходим в папку codebase берем ее содержимое и переносим в паку libs.
Рис. 11 – Содержимое папки «libs»
После того как наполнили содержимым папку «libs», открываем файл «renderer.js» и в нем пишем следующее:
// Подключаем модуль электрона
const { remote } = require('electron')
// Получаем указатель на окно браузера где будет производиться отображение нашего интерфейса
let WIN = remote.getCurrentWindow()
// Подключаем фреймворк webix
const webix = require('./libs/webix.min.js')
//Подключаем фреймворк JQuery
$ = require('jquery')
// Функция, в которую в качестве параметра как раз и будет передаваться наша с Вами верстка
webix.ui({
})
Далее приступаем к верстки нашего интерфейса. Для чего переходим вот сюда designer.webix.com/welcome жмем на кнопку «Quick Start» и вперед к верстке интерфейса. например такого какой показан на рисунке 12.
Рис. 12 – Верстка интерфейса в Web конструкторе Webix
Верстка, показанная на рисунке 12 очень проста, и заключается в перетаскивании мышкой нужного элемента в рабочую область конструктора. Правая часть конструктора предназначена для редактирования свойств элементов. Когда верстка закончена (рис. 13) необходимо ее перенести в файл renderer.js. Для чего жмем на кнопку «Code» и получаем готовую верстку в виде текста. Выделяем этот текст и копируем его.
Рис. 13 – Преобразование верстки в текст
После чего открываем файл «renderer.js» в «VSC» и вставляем в заранее подготовленную функцию «webix.ui» скопированный текст (рис. 14).
// Подключаем модуль электрона
const { remote } = require('electron')
// Получаем указатель на окно браузера где будет производиться отображение нашего интерфейса
let WIN = remote.getCurrentWindow()
// Подключаем фреймворк webix
const webix = require('./libs/webix.min.js')
//Подключаем фреймворк JQuery
$ = require('jquery')
// Функция в которую в качестве параметра как раз и будет передаваться наша с Вами верстка
webix.ui(
{
"id": 1587908357897,
"rows": [
{
"css": "webix_dark",
"view": "toolbar",
"height": 0,
"cols": [
{ "view": "label", "label": "Elcetron +Webix its cool!"},
{ "label": "-", "view": "button", "height": 38, "width": 40},
{ "label": "+", "view": "button", "height": 38, "width": 40},
{ "label": "x", "view": "button", "height": 38, "width": 40}
]
},
{
"width": 0,
"height": 0,
"cols": [
{ "url": "demo->5ea58f0e73f4cf00126e3769", "view": "sidebar", "width": 177 },
{
"width": 0,
"height": 0,
"rows": [
{ "template": "Hello WORLD! ", "view": "template" },
{
"url": "demo->5ea58f0e73f4cf00126e376d",
"type": "bar",
"xAxis": "#value#",
"yAxis": {},
"view": "chart"
}
]
}
]
}
]
}
)
Рис. 14 – Добавление верстки созданной с помощью конструктора
Потерпите еще немного. Далее берем и открываем в «VSC» наш заранее подготовленный файл index.html и дописываем туда буквально три следующие строки: , и
Файл index.html примет следующий вид:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="stylesheet" href="./libs/webix.css">
<link rel="stylesheet" href="./style.css">
<title>Document</title>
</head>
<body>
<script src="./renderer.js"></script>
</body>
</html>
Далее жмем «F5» и получаем наше приложение с версткой от Webix (рис. 15)
Рис. 15 – Рабочее окно приложения с версткой от Webix
Добавляем пару фишек
Все вроде работает. Но хотелось бы убрать не нужное нам с вами обрамление окна, которое не очень то вписывается в наш дизайн. Мы будем использовать с вами кастомное решение. Для чего открываем файл «main.js» и добавляем туда параметр frame:false при создании BrowserWindow, данный флаг убирает стандартное меню и обрамление окна. Должно получиться вот так:
const win = new BrowserWindow({
width: 800,
height: 600,
frame:false,
webPreferences: {
nodeIntegration: true
}
})
Жмем на «F5» смотрим на результат без рамочного окна (рис. 16).
Рис. 16 – Рабочее окно приложения с версткой от Webix
Осталось научить наше окно реагировать на событие от мышки. В начале сделаем заголовок активным, чтобы за него можно было перемещать наше окно по экрану монитора. Для чего открываем файл «renderer.js» находим в нем элемент view:label и добавляем в него свойство css:”head_win” должно получиться как показано на рисунке 17.
{ "view": "label", "label": "Elcetron +Webix its cool!", css:"head_win" },
{ "label": "-", "view": "button", "height": 38, "width": 40},
{ "label": "+", "view": "button", "height": 38, "width": 40 },
{ "label": "x", "view": "button", "height": 38, "width": 40}
Рис. 17 – Добавление стиля к элементу view:label
Теперь необходимо собственно этот стиль прописать в файле созданном специально для этих целей. Открываем файл «style.css» и создаем следующий стиль:
.head_win {
-webkit-app-region: drag;
}
После чего запускаем приложение «F5» и пробуем перетащить наше окно за заголовок. Должно все работать.
Напоследок наше приложение реагировать на нажатие мышкой на кнопки заголовка «-,+,x» окна. Для чего в конец файла «renderer.js» добавим следующий код:
// Закрытие окна
$$("close-bt").attachEvent("onItemClick", () => {
const window = remote.getCurrentWindow();
window.close();
})
// Свернуть окно
$$("min-bt").attachEvent("onItemClick", () => {
const window = remote.getCurrentWindow();
window.minimize();
})
// Распахнуть окно
$$("max-bt").attachEvent("onItemClick", () => {
const window = remote.getCurrentWindow();
if (!window.isMaximized()) {
window.maximize();
} else {
window.unmaximize();
}
})
В этом коде запись вида «$$(“max-bt”)» означает, обращение к элементу «Webix» по его «id». Поэтому необходимо для кнопок заголовка эти id прописать в файле «renderer.js», что мы и сделаем должно получиться как показано на рисунке 18:
{ "view": "label", "label": "Elcetron +Webix its cool!", css:"head_win" },
{ "label": "-", "view": "button", "height": 38, "width": 40, id:"min-bt" },
{ "label": "+", "view": "button", "height": 38, "width": 40, id:"max-bt" },
{ "label": "x", "view": "button", "height": 38, "width": 40, id:"close-bt" }
Рис. 18 – Добавление стиля к элементу view:label
На этом пока все. Пробуйте должно работать. Исходный код можно скачать с гитхаба, вот ссылка. Благодарю за внимание.
serf
remote модуль тоже желательно не использовать, он уже диприкейтнут.
Вместо этого следует использовать IPC коммуникацию в явном виде, то есть без nodeIntegration / remote хаков.
kocmoc-dev Автор
Ok
kocmoc-dev Автор
Может я ошибаюсь, но «nodeIntegration: false» актуально при отображении удаленного контента это написано вот тут. Если приложение работает локально то почему нет?
Zoolander
все верно сказали. Запрещать node integration нужно для BrowserWindow that loads remote content.
Zoolander
// nodeIntegration: true
// не делайте так.
Обоснуйте. Любое инженерное решение не является абсолютным и подразумевает контекст.
В некоторых контекстах — проще влепить nodeIntegration: true без всяких угроз безопасности. Это наше приложение, наш код, мы его контролируем. Если есть доступ чужого JS-скрипта, удаленной веб-страницы — ну это вопрос к вам, почему и зачем вы это допускает в Электроне. Тогда да, ставьте nodeIntegration: false и городите усложненную архитектуру с делегацией запросов.
Программисты — это инженеры, а не веруны в догмы. Любое утверждение должно иметь четко описанный контекст применения.
serf
Причин много, но даже одна нижеуказанная уже является достаточной.
Если используется подход «nodeIntegration: true», то архитектура как таковая отсутствует и следовательно там нечего усложнять. Использовать подход «nodeIntegration: true» это как делать прямые запросы к базе данных из PHP шаблона в 2020 году.Чаще всего в крупных/серьезных проектах существует разделение на фронтендеров и бэкендеров, со своей зоной ответственности. Включение флага nodeIntegration позволяет фронтендерам выходить за пределы своих полномочий. Поэтому общение между процессами main/node и renderer/web должно происходить через лимитированный набор функций / API. То есть получается архитектура клиент/сервер. В итоге ответственности разделены и наружу из main процесса выставляется только определенный функционал, безопасность обеспечить проще. Кроме этого получаем побочные преимущества тк веб не завязан жестко на прямое использование API ноды/электрона: 1) например проще мокнуть этот API и тестировать веб часть автономно 2) в случае надобности проще поменять/переключить исполнителя запроса от фронтенда или даже заменить электрон на что-то похожее имея тот же веб. 3) и тд.
avengerweb
Крутое слово архитектура, можно вставить его в любой пост, упомянуть многострадальный php и готово. Наличие лишнего слоя ни чего не говорит о безопасности, это просто лишний слой, который заставляет вас сеарилизовать и десеарилизовывать данные и поверьте это далеко не последнее место где допускают ошибки… А если ваш код это не тривиальный туду лист, то тут ещё выходит на сцену адекватное потребление ресурсов. А Про мокать и тестить, тут уже совсем смешно, если конечно вы подцепляете fs и читаете файлик с диска прямо рядышком с вашим шаблоном, то это немного другое, но как только у вас появляется отдельные сервисы для вашей бизнес логики, то ни проблем с переносом кода во что то другое ни тем более в веб, не возникает. Но про веб это чаще сказки, приложений которые так смогли — единицы.
serf
Сериализация происходит в любом случае, не все это понимают. В случае использования обертки / API взаимодействия, например, механизм сериализации можно изменить так как существует единая точка входа в логику обмена данными между процессами, например паковать данные в MessagePack формат (иногда это приоритетный вариант).
При «nodeIntegration: true» это технически возможно. То что технически возможно когда-то происходит, не важно по каким причинам.
Те кто использует «nodeIntegration: true» как правило не оборачивают весь вызываемый из main/node/backend процесса функционал в фасад. Поэтому там каша, мешанина веб и бэкенд логики. Такую кашу, как правило, в случае неободимости разгребать/разделять не просто.
megahertz
Соглашусь, надо в первую очередь понимать что и зачем. А то частенько попадаются ситуации когда nodeIntegration: false, а следом в preload.js window.require = require.
serf