[Из песочницы] Собираем грабли Electron.js или десктопные JS-приложения на практике

image

Electron — система позволяющая создавать кроссплатформенные приложения используя одни только веб-технологии, такие как HTML, CSS и конечно, JS.

Нужно отметить, что разработка на Электроне очень во многом отличается от обычного браузерно-серверного приложения на Node. О чем и будет эта статья.
За подробным введением в Электрон добро пожаловать сюда и туда.

Около полугода назад я узнал об Электроне. Тогда я реализовал простенький платформер на базе Phaser. Js, и почувствовал полный восторг от того насколько быстро и удобно эта платформа позволяет создавать приложения. Впрочем, этот опыт был отложен до лучших времен.

Тогда мне пришлось работать лишь с Canvas, а вся основная логика создавалась средствами Phaser и подключением скриптов. Никакой особой архитектуры приложению не требовалось. Однако неделю назад возникла потребность в небольшой десктопной утилите, и выбор пал на Электрон как на платформу для разработки. И тут заверте…

Да, для тех кто заинтересуется разработкой чего-нибудь играбельного на Электроне: WebGL приложение работает быстрее чем в обычном браузере. Как ни странно, хуже всех себя показывает Chrome, на котором Электрон и основан. Впрочем, разница не намного велика.

Вот график по кадрам в секунду (бенчмарком был этот пример, с отключенным удалением объектов):

image

Что ж, вперед. Попробуем написать что-нибудь толковое.

Первоначально я задумался об элементарном удобстве разработки. Нужна была хоть какая-то архитектура, и я полез освежать свои знания о JS-фреймворках. Выбор пал на Backbone.js. Он известен своей легкостью, и, что в данном случае является гигантским плюсом, совершенно не привязан к вебу. Backbone, на мой взгляд, может быть отличной основой как для серверного приложения, так и для браузера и собственно десктопа.

Как я говорил, Electron — это не то же самое что Chrome и отвечающий ему локально запущенный Node.

Если уходить чуть глубже в строение Электрона, то это скорее обрезанный по части браузерных особенностей Chromium и встроенный в него Node.js. (не путать с «голым» V8 в обычной поставке браузера).

image

Выполнение приложения начинается с такого кода:

//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"); 


Забегая вперед, скажу что удалось реализовать динамическую загрузку/выгрузку модулей, но об этом как-нибудь в следующий раз.

© Habrahabr.ru