[Из песочницы] Собираем грабли Electron.js или десктопные JS-приложения на практике
Electron — система позволяющая создавать кроссплатформенные приложения используя одни только веб-технологии, такие как HTML, CSS и конечно, JS.
Нужно отметить, что разработка на Электроне очень во многом отличается от обычного браузерно-серверного приложения на Node. О чем и будет эта статья.
За подробным введением в Электрон добро пожаловать сюда и туда.
Около полугода назад я узнал об Электроне. Тогда я реализовал простенький платформер на базе Phaser. Js, и почувствовал полный восторг от того насколько быстро и удобно эта платформа позволяет создавать приложения. Впрочем, этот опыт был отложен до лучших времен.
Тогда мне пришлось работать лишь с Canvas, а вся основная логика создавалась средствами Phaser и подключением скриптов. Никакой особой архитектуры приложению не требовалось. Однако неделю назад возникла потребность в небольшой десктопной утилите, и выбор пал на Электрон как на платформу для разработки. И тут заверте…
Да, для тех кто заинтересуется разработкой чего-нибудь играбельного на Электроне: WebGL приложение работает быстрее чем в обычном браузере. Как ни странно, хуже всех себя показывает Chrome, на котором Электрон и основан. Впрочем, разница не намного велика.
Вот график по кадрам в секунду (бенчмарком был этот пример, с отключенным удалением объектов):
Что ж, вперед. Попробуем написать что-нибудь толковое.
Первоначально я задумался об элементарном удобстве разработки. Нужна была хоть какая-то архитектура, и я полез освежать свои знания о JS-фреймворках. Выбор пал на Backbone.js. Он известен своей легкостью, и, что в данном случае является гигантским плюсом, совершенно не привязан к вебу. Backbone, на мой взгляд, может быть отличной основой как для серверного приложения, так и для браузера и собственно десктопа.
Как я говорил, Electron — это не то же самое что Chrome и отвечающий ему локально запущенный Node.
Если уходить чуть глубже в строение Электрона, то это скорее обрезанный по части браузерных особенностей Chromium и встроенный в него Node.js. (не путать с «голым» V8 в обычной поставке браузера).
Выполнение приложения начинается с такого кода:
//main.js
//Основная конфигуация для старта приложения
const electron = require('electron');
const app = electron.app;
const BrowserWindow = electron.BrowserWindow;
let mainWindow;
function createWindow () {
// Create the browser window.
mainWindow = new BrowserWindow({
width: 800,
height: 600,
//fullscreen:true,
frame:false,
resizable:false
}); //основная конфигуация
mainWindow.loadURL('file://' + __dirname + '/views_html/startscreen/index.html'); //загрузка html файла
mainWindow.on('closed', function() {
mainWindow = null;
});
} //закрытие главного окна
app.on('ready', createWindow); //создание окна при готовности приложения
app.on('window-all-closed', function () {
if (process.platform !== 'darwin') {
app.quit();
}
}); //закрытие окна и сворачивание в док если это OS X
app.on('activate', function () {
.
if (mainWindow === null) {
createWindow();
}
}); //восстановление окна
Более подробный разбор по ссылкам в начале статьи.
Этот код выполняется в Node, со всеми полагающимися недостатками и преимуществами. На основе его работы порождается окно с WebKit и средой для выполнения JavaScript. Средой является тот же самый Node, запустивший приложение, однако инициализирующий код и код вашего приложения между собой взаимодействовать не смогут.
Проще говоря, main.js создает конфигуацию окна и отвечает за интеграцию приложения в систему. Index.html — это точка входа уже непосредственно в разработанное Вами приложение. Взаимодействовать между собой напрямую они не могут. Передать переменную из main в скрипт подключенный к загруженной html не получится, несмотря на то что они выполняются друг за другом на одной виртуальной машине.
Но не напрямую возможно. Существует целых два способа передачи данных из инициализиующего кода в само приложение. Первый довольно очевиден, регистрация глобального объекта. Одним из таких является process, и он содержит в себе всю информацию о среде в которой приложение выполняется:
!DOCTYPE html>
Hello World!
Hello World!
Мы используем Node ,
Chrome ,
и Electron .
Выполнение данного кода выведет на экран версию Node, Cromium и Electron.
Естественно можно добавить свой объект и выводить таким же образом.
И второй способ. Выполнение кода внутри Вашего приложения, своеобразный RPC. Это будет выглядеть так:
mainWindow.webContents.executeJavaScript( //указываем окно где скрипт должен выполниться
`alert("Hello from runtime!");`); //код на выполнение
Здесь стоит учитывать, что:
Код, указанный в executeJavaScript (), выполнится только после загрузки страницы. Поэтому нельзя таким образом инициализировать переменные критичные для старта приложения. Есть, конечно, вариант и само приложение таким образом запускать, обернув стартовый скрипт в функцию и:
mainWindow.webContents.executeJavaScript( //указываем окно где скрипт должен выполниться
`alert("Hello from runtime!");`; //код на выполнение
start_up(); //Пуск
);
Это явно не лучшее решение.
Честно говоря, такой подход меня несколько удивил. На мой взгляд, было бы гораздо удобнее некоторые вещи, вроде загрузки файлов конфигурации и прочие, не меняющиеся по ходу выполнения части, загружать вместе с инициализацией окна.
Однако я недаром упомянул что «браузерная» часть приложения также выполняется внутри Node, следовательно вы имеете полное право использовать модули, как установленные при помощи npm:
npm install backbone
npm install jquery
var Backbone = require('backbone');
var $ = require('jquery');
Так и подключать собственные части приложения при помощи:
require("path_to_file");
Но здесь поджидает гигантский подводный камень.
При обычной разработке на Node.js мы можем подключить файл таким образом:
require("./app");
Это код укажет приложению что нужно включить файл app лежащий в той же директории что и выполняющийся скрипт. Но в «браузерной» части Электрона не работают пути, которые начинаются со слэша или »./».
По умолчанию он ищет в папке node_modules в корне приложения, и если вы укажете:
require("modules/app"); //сработает при условии что папка modules находится в node_modules
require("/modules/app"); // => Error
require("./modules/app"); // => Error, даже если папка рядом
В тоже время:
require("modules/../app"); // если app лежит в node_modules
отработает как надо.
Будьте внимательны.
Естественно, меня такое положение не устроило. Раскидывать приложение по папочкам в директории для модулей рантайма решительно не хотелось. Я вышел из ситуации так:
function require_file(file) {
require("modules/../../js/app/modules/"+file); //вместо modules может быть абсолютно любая папка в node_modules
}
Костыль? Однозначно. Зато теперь я могу включать необходимые мне части приложения указав:
require_file("show"); //где show - необходимый модуль
Вернемся к нашей структуре. Я выбрал такой подход:
Файл system_init единственный подключаемый скрипт в html-файле.
В нем находится:
var Backbone = require('backbone'); //инициализация библиотек
var $ = require('jquery');
Ряд других функций-хелперов вроде загрузки файлов конфигурации и запуск приложения:
var router = new APP.SystemRouter({
menus: new APP.SystemCollection()
});
Дальше реализация классического backbone приложения, с вынесением логически отдельных частей в модули и подключение их при помощи:
require_file("/path/module");
Забегая вперед, скажу что удалось реализовать динамическую загрузку/выгрузку модулей, но об этом как-нибудь в следующий раз.