Собираем грабли 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>
<html>
  <head>
    <meta charset="UTF-8">
    <title>Hello World!</title>
  </head>
  <body>
    <h1>Hello World!</h1>
    Мы используем Node <script>document.write(process.versions.node)</script>,
    Chrome <script>document.write(process.versions.chrome)</script>,
    и Electron <script>document.write(process.versions.electron)</script>.
  </body>
</html>

Выполнение данного кода выведет на экран версию 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 - необходимый модуль

Вернемся к нашей структуре. Я выбрал такой подход:

<!-- index.html -->
<script type="text/javascript" src="../../js/app/system_init.js"></script>

Файл system_init единственный подключаемый скрипт в html-файле.

В нем находится:

var Backbone = require('backbone'); //инициализация библиотек
var $ = require('jquery');

Ряд других функций-хелперов вроде загрузки файлов конфигурации и запуск приложения:


var router = new APP.SystemRouter({
      menus: new APP.SystemCollection()
    });

Дальше реализация классического backbone приложения, с вынесением логически отдельных частей в модули и подключение их при помощи:

require_file("/path/module"); 

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

Similar posts

Comments 25

    0
    NW.js не рассматривался? Вроде как дает меньший размер приложения на выходе.
      +1
      У электрона есть свои преимущества, Как упомянул ниже, для меня это ES6(кстати, не понял, почему не работает в nw.js, если у него на данный момент хром посвежее. Включать нужно, что ли?), и более полноценная консоль(к примеру, то же сохранение css в nw.js не работает). Ну и electron-rebuild — удобная штука(не в курсе, есть ли аналоги у nw).

      Что до размера — 123МБ у свежего nw.js, 120 — у электрона.
        0
        Рассказываю про ECMAScript 6 в NW.js: поддержка этой версии языка JavaScript будет в новой версии NW.js (в версии v0.13.0), выпуск которой ещё не состоялся, но ужé вот-вот состоится. А последняя стабильная версия (0.12.3) основана на движке IO.js v1.2.0 — это довольно старая версия нодовского движка, именно поэтому в ней всё плохо с ECMAScript 6.

        Кто не хочет дожидаться выпуска новой версии, тот может поставить себе v0.13.0-rc1 на пробу (скачать по гиперссылке с Гитхаба или установить командою «npm install nw@0.13.0-rc1sdk» как npm-пакет). Сразу предупреждаю, что rc1 ещё не годится в продакшен из-за неустранённых багов (заголовок окна выглядит не лучшим образом, например).
          0
          Ну и конечно же еще никто не мешает добавить babel в проект :)
        +1
        Основными плюсами Электрона для меня были ES6 и, самое главное, документация. Она у Electron ощутимо лучше
        0
        По поводу путей — в чем проблема использовать тот же __dirname? Да и насчет точки не понял, откуда такая информация? Кусок из html текущего проекта:

        <script>window.$ = window.jQuery = require('./web/libs/jquery/dist/jquery.min.js');</script>

        Возможно, такая проблема возникает только внутри скриптов, подключаемых через require или index.html лежит в корне, а не в какой-либо папке? По крайней мере, с такой проблемой не сталкивался.
        Кстати, еще не помешало бы упомянуть, что как минимум jQuery сходу не заведется(если не ошибаюсь, связано с проверкой на окружение) и вышеупомянутая строчка как раз является фиксом.

        По поводу взаимодействия main.js/index.html и передачи переменных напрямую не совсем верно. Точнее говоря, напрямую передать нельзя, а вот посредством ipc — пожалуйста.
        https://github.com/atom/electron/blob/master/docs/api/ipc-main.md
        https://github.com/atom/electron/issues/991
        Кстати говоря, это же справедливо и для webview.

        Еще бы не помешало упомянуть про то, что index.html не обязательно должен находиться на локальной машине. Довольно удобным способом является использование BrowserWindow в качестве обычного браузера, но с подключением скрипта с нодой с указанием через параметр preload и отключением контекста ноды для безопасности(ну или без отключения, если таковая не требуется).

        Насчет глобального объекта — не уверен, сработает ли такой фокус. Разве если в main.js добавить свойство в тот же process, оно будет доступно в "клиентской" части? Если да — спасибо, пригодится.

        Про выгрузку модулей было бы интересно почитать, хотя не совсем понятно, зачем это нужно, с учетом того, что при обновлении страницы перезагружается и нода.

        Ну и главное — планируется ли написание других статей по электрону? Перешел на него с nw.js(приманило наличие ES6 из коробки и рабочее сохранение файлов из devtools), было бы интересно почитать статьи об особенностях использования.
          0
          Насчет глобального объекта — не уверен, сработает ли такой фокус. Разве если в main.js добавить свойство в тот же process, оно будет доступно в «клиентской» части? Если да — спасибо, пригодится.

          На самом деле, и main.js и index.html выполняются node. Но, main.js и index.html для node — это разные модули. Совершенно справедливо, что переменные одного модуля не видны в других модулях. Иначе, подключив все нужные модули npm, мы бы получали кашу из переменных, и могли бы сломать работу, если переменные определены в разных модулях дважды.

          Передавать переменные в index.html лучше через свойство global чем через свойство process. Так и логичнее, и доступ к переменной будет короче на длину "process.".

          Переменная, определенная как свойство global будет доступна для всех модулей, выполняющихся после определения. Но, не стоит бояться "перегрузки" глобальных имен локальными в других модулях. Например, такое использование будет работать в штатном режиме:

          // module1.js
          global.a = "hello";
          
          // module2.js
          console.log(a);  // => hello
          
          // module3.js
          var a = 0;
          console.log(a);  // => 0
            0
            Еще добавлю, что backbone можно подключать через require(), но jQuery я бы подключал через тэг <script src=""></script>.

            Кстати, разницы в поведении require() в main.js и index.html нет, если они находятся в одной папке.
              0
              По поводу jQuery выше отписывался — в electron из-за наличия module и window единовременно jQuery начинает колбасить. потому приходится изгаляться. Напрямую через скрипт просто так не подключишь.
              Еще смешнее получается при использовании preload и включенной ноде. Как в таком случае поступать. так и не придумал. в итоге тупо выключил ноду для окна(благо. в скрипте из preload контекст ноды доступен в любом случае).

              Про global для ноды в курсе. но не знал. что это справедливо и для BrowserWindow, спасибо.
                0
                Если не трудно, опишите в чем проявляется колбас jQuery? В последний раз (уже довольно давно) на nw делал десктопное приложение. Клиентского js, как такового, я не писал. Но был подключен jQuery и Bootstrap. И, вроде бы, все bootstrap-овское работало как часы без костылей.
                  0
                  Опытным путем я выяснил что нормально скриптово подключенная jQuery работает только версии 1.7.1. Даже последняя 1.12 (в первой ветке последняя) уже черт те что творит. Функции могут отказывать, могут не срабатывать совершенно не записывая ничего в лог, может попросту не инициализироваться, даже вручную.

                  Самое оптимальное — подключать модулем. Тем более что с точки зрения здравого смысла это гораздо логичнее, чем подключать ссылкой непосредственно к разметке.
                    0
                    Да, я тоже уже нашел.
                      0
                      jquery спокойно следует UMD, и подключать его можно как хотите. К тому же используя сборщики, подключать jquery в html дико :)
                    0
                    В nw.js такой проблемы нет, насколько мне известно.
                    В electron немножко другой принцип работы. Пример ошибки и решения можно посмотреть хотя бы и здесь — https://github.com/atom/electron/issues/254. Судя по описанию, проблема состоит в том, что jQuery более поздних версий может работать как в браузерном контексте, так и в CommonJS. Подстава в том, что в electron'е jQuery видит наличие module и пытается работать как в CommonJS, не объявляя переменной jQuery/$ в глобальной области видимости. Там же можно подсмотреть обычное предлагаемое решение — window.$ = window.jQuery = require('/path/to/jquery');.
                    Подстава еще в том, что если использовать не локальный файл, а подгружать скрипт preload'ом, то происходит какая-то хитрая балалайка: jQuery вываливается на функции assert с гениальным уведомлением о том, что document не объявлен. Пробовал фиксить сам jQuery и хитрыми шаманскими методами, но за пару часов так и не получилось ничего наковырять, а лезть дальше в кишки библиотеки с дебаггером было откровенно лень. Благо что в самом BrowserWindow мне контекст ноды был не нужен, а в preload'е не нужен был jQuery, так что просто отключил контекст ноды у окна.

                    Что до разницы версий(DarthGelum) — спасибо, надо будет проверить. Я так понимаю, эту проверку на module добавили в очередной версии jQuery, возможно, более старые версии будут работать адекватно.
                0
                Проблема являла себя вне зависимости от положения index.html.
                jQuery 1.7.1 заводится абсолютно без проблем (не знаю почему именно эта версия, попробую вычислить причину при случае).
                Фокус с глобальным объектом работает, но это убогое решение. Глобальные, тем более объекты, — зло.
                Да, добавление свойств в process тоже работает, это стоит иметь в виду, я согласен.

                Я бы хотел написать еще статей по Электрону. Честно говоря, я обнаружил много любопытных деталей. Постараюсь сделать это когда наберется достаточно материала и возможность соединить это в логичное повествование.
                0
                А кто-нибудь может подсказать, сколько требует минимально памяти Electron для удовлетворительной работы? Или может у кого-нибудь есть бенчмарки?
                  0
                  У меня электрон в относительно простом приложении зачем-то держит около 5 процессов суммарно метров на 100.
                    0
                    Какая ОС? 64бит?
                      0
                      Угу.
                      0
                      куда катиться этот мир?

                      у самого простейшее NW.js приложение жрёт 65МБ ОЗУ и весит 88МБ на диске
                      0
                      У меня 64 бит Linux Mint, Электрон создает три процесса при отладке, и два в билде. Памяти суммарно около 60-80 мб.
                      0
                      В electron приложениях можно же использовать webpack или browserify для сборки "клиентского" кода, как раз, чтобы не переживать о том, как работает require.
                        0
                        Какой в среднем вес программы получается, около 120 МБ?
                          0
                          На разные ОС по разному.
                          Версия для Linux в распакованном виде 105.9Mb.
                          Чтобы уменьшить вес программы, можно собирать Electron вручную. Серьезного профита можно добиться выключив поддержку ES2015 в ключах сборки v8. Да и вообще, из v8 можно много чего выкинуть…
                            0
                            И если всё выкинуть, то сколько в результате вес получится?

                        Only users with full accounts can post comments. Log in, please.