Современный Electron приложение состоит из трех модулей:

  • main;

  • renderer;

  • preload;

Каждый из этих модулей выполняется в собственном контексте и среде. Учитывая это ваш проект может быть организован как моно репозиторий, где каждый модуль — отдельный пакет со своими настройками, зависимостями, тестами и системой сборки (или вообще без нее).

main

  • Среда выполнения: Node.js.

  • Поддержка ESM: Нет.

  • Полный доступ к Electron API.

Это backend вашего приложения и точка входа — с этого модуля начинается запуск. Код в этом модуле это обычный JavaScript который выполняется в Node.js. Именно здесь вы должны описать когда создавать окна программы, с каким содержимым, с какими параметрами, проверять обновления, отслеживать события и выполнять завершения вашего приложения — по умолчанию Electron не делает ничего за вас.

Примечание: Хотя Node.js уже имеет поддержку ESM на момент написания этой статьи вы все еще не можете использовать ES модули в среде выполнения Electron. Так что, вам придется использовать Commonjs синтаксис, или транспилировать свой код из ESM в CJS.

Базовый код этого модуля выглядит следующим образом:

const {BrowserWindow, app} = require('electron')

// Дождаться полной инициализации Electron
// Только после этого возможно создавать окна
app.whenReady().then(() => {
    // Создаёт новое окно браузера
    const win = new BrowserWindow({
        show: false // Пока-что окно не нужно показывать пользователю
    })

    // Загружаем в окне веб-содержимое
    win.load('index.html')

    // Когда веб-страница будет загружена и отрисована 
  	// — показать окно пользователю
    win.once('ready-to-show', win.show)
})

// Завершить работу приложения 
// если пользователь закрыл все окна программы
app.on('window-all-closed', app.quit)

Этот код создаст новое окно браузера, в котором будет загружено file://path/to/app/index.html .

Обратите внимание, что по умолчанию веб-содержание открываться по протоколу file:, что накладывает определенные ограничения. Чтобы обойти эту проблему, зачастую, программа регистрирует собственный, произвольный протокол и загружает страницу через него.

const {protocol} = require('electron');
const path = require('path')

protocol.registerFileProtocol('my-cool-app', (request, respond) => {
    let requestedResource = new URL(request.url).pathname;
    respond( path.resolve('path/to/files', requestedResource) );
});

// ...

win.load('my-cool-app://index.html')

Подробнее об произвольных протоколах

renderer

  • Среда выполнения: Chromium.

  • Поддержка ESM: Да.

  • Нет прямого доступа к API Node.js или Electron.

Каждый раз, когда вы создаете новое окно вызывая BrowserWindow электрон порождает новый процесс Renderer с тем содержимым, которое вы передали (win.load('my-cool-app://index.html')').

Этот модуль — обычный веб-сайт. И работать с ним можно точно так же: HTML/CSS/JS.

Так же как и привычные веб-страницы этот модуль, хотя и поддерживает ESM, не имеет прямого доступа к npm пакетам или к файловой системе. А значит вам нужно включать все зависимости в свои JS бандлы с помощью таких инструментов как webpack, rollup, vite и тому подобное. И загружать каждый бандл через произвольный протокол, как было показано выше.

Кроме этого скрипты в renderer не имеют прямого доступа к Node.js API. Единственный способ взаимодействовать с системой пользователя - использовать интерфейсы описаны в модуле preload как прослойку.

preload

  • Среда выполнения: Chromium.

  • Поддержка ESM: Нет.

  • Полный доступ к Node.js API.

  • Частичный доступ к Electron API.

preload - это особые JS сценарии, которые будут выполняться перед каждой загрузкой веб-страницы.

Подключается он индивидуально для каждого окна:

new BrowserWindow({
    webPreferences: {
        preload: 'preload.js'
    }
}) 

Предназначен этот модуль для создания узких, контролируемых интерфейсов через которые renderer сможет взаимодействовать с Node.js API:

  1. В preload создаете глобальный метод.

  2. А в renderer используете его. Благодаря замыканию метод созданный в preload будет иметь доступ к Node.js даже если его вызвали в renderer.

Примечание: preload выполняется изолированно от renderer. Это означает, что globalThis в preload не тот же что в renderer. Для передачи глобальной переменной с одного контекста в другой существует специальное API: contextBridge.exposeInMainWorld(key, value).

Например:

// preload.js

const {contextBridge} = require('electron')
const {readFile, writeFile} = require('fs/promises')

contextBridge.exposeInMainWorld('settingsAPI', {
    getSettings: () => readFile('path/to/user-settings.json').then(JSON.parse),
    saveSettings: (value) => writeFile('path/to/user-settings.json', JSON.stringify(value)),
})

Вызов этого кода в контексте preload создаст глобальную переменную settingsAPI для контекста renderer:

// renderer.js

// Возвращает результат чтения с файловой системы
globalThis.settingsAPI.getSettings()

// Записывает данные в файловою систему
globalThis.settingsAPI.saveSettings({foo: 'bar'})

Вам может показаться хорошей идеей просто передать в renderer полный доступ к Node.js API:

const fs = require('fs/promises')

contextBridge.exposeInMainWorld('fs', fs)

Но это нарушением требований к безопасности программы, о которых я поговорю позже.

Несмотря на то, что preload имеет прямой доступ к Node.js API и к npm пакетам, он все же имеет ограниченный доступ непосредственно к Electron API. Например, вы можете таким же образом использовать crashReporter api но не можете dialog api.

В документации для всех встроенных API указано в каком процессе он может работать: Main или Renderer.

Обратите внимание, даже если в документации указано, что вы можете использовать что-то в процессе Renderer, это не значит, что вы можете вызвать это в модуле renderer, поскольку вы просто не сможете импортировать необходимое из Electron.

// renderer.js
// Приведёт к ошибке, поскольку renderer не имеет доступа к require
const {crashReporter} = require('electron')

Вам все еще нужно импортировать необходимые API в preload а затем передать соответствующий интерфейс к renderer:

// preload.js
const {crashReporter} = require('electron')

contextBridge.exposeInMainWorld('crashReporter', {
    start: () => crashReporter.start()
})
// renderer.js
globalThis.crashReporter.start()

А для использования всех других API необходимо отправлять в Main процесс сообщения о намерениях и вызвать соответствующие API там:

// renderer.js
// Вернёт результат выполнения из Main 
globalThis.dialogs.showMessageBox('Message text') 
// preload.js

const {contextBridge, ipcRenderer} = require('electron')

// preload не может создать диалог самостоятельно, 
// поэтому отправляет соответствующую команду в Main
contextBridge.exposeInMainWorld('dialogs', {
    showMessageBox: (message) => ipcRenderer.invoke('dialogs', message)
})
// main.js
const {dialog, ipcMain} = require('electron')

// Main слушает все входящие сообщения
ipcMain.handle('dialogs', (event, message) => {
	// Создаёт диалог и отправляет результат в preload.js
	return dialog.showMessageBox({message})
})

Требования к безопасности

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

Вот некоторые требования.

Создавайте минимальные интерфейсы в preload

Почему нельзя передать в renderer все методы сразу?

const fs = require('fs/promises')

contextBridge.exposeInMainWorld('fs', fs)

Дело в том, что доступ к этому api получаете не только вы, а абсолютно все содержимое окна. То есть любые посторонние скрипты, виджеты, встроенные видео плееры, фреймы, веб-сайты открытые в этом окне или в дочерних окнах, все npm пакеты включены в ваш бандл, код которых вы не контролируете — все это будет иметь полный и не контролируем доступ к файловой системе.

Представьте, браузер, который позволяет любым сайтам записывать или читать что-угодно из вашего диска.

Весь доступ к системе заблокирован по-умолчанию. В preload необходимо открывать минимально необходимые API и строго проверять все входные параметры.

Используйте песочницу когда возможно

new BrowserWindow({
    webPreferences: {
        sandbox: true
    }
})

Режим песочницы в значительной степени ограничивает ваше приложение на уровне операционной системы. Кроме того, это ограничивает те системные Node.js API которые можно использовать в preload:

  • events

  • timers

  • url

Используйте Content Security Policy

Как и для интернета CSP это мощный инструмент для защиты от "cross-site-scripting". Он позволяет определить из каких источников позволить загрузку и выполнение содержания.

По умолчанию вам следует запретить абсолютно все:

  • Запретить загружать и выполнять JS который не был включен в вашу программу.

  • Запретить обращаться к любым внешним источникам.

  • Запретить загружать любые внешние ресурсы, такие как изображения, видео, файлы и тому подобное.

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

Установить политику можно с помощью мета тега. Например:

<meta 
    http-equiv="Content-Security-Policy" 
    content="script-src 'self' https://apis.example.com"
>

Разрешает выполнение только встроенных скриптов и тех, которые были загружены с домена apis.example.com по защищенному протоколу https. Все остальные скрипты будут заблокированы.

Подробнее о Content Security Policy

Комментарии (5)


  1. Kalinavich
    17.09.2021 15:12

    Более гибко получается чем в QT, сравните + и - в следующей статье плиз


    1. HemulGM
      21.09.2021 19:00

      Что тут гибко? Здесь даже сравнивать нет смысла с Qt. В сравнении с Qt здесь вообще нет гибкости. В Qt ты можешь строить приложение как хочешь. Строить интерфейс как хочешь. MVVM, MVC, без паттерна. Использовать прямой доступ к ОС или через API фреймворка. Использовать возможности фреймворка Qt или делать как вздумается. Выполнять код на процессоре или использовать интерпретаторы и код на любом другом языке в песочнице. О какой лучшей гибкости, чем в Qt может идти речь, если ты можешь сделать приложение с архитектурой Electron в Qt?

      И так с множеством других языков и фреймворков. Delphi, C#

      О каких плюсах может идти речь, если в Qt и других фреймворках ты так же можешь повторить "Электрон". Плюс тут только один и сомнительный - Электрон уже сделали и ты можешь писать ужаснейшие приложение сразу.

      Но я могу перечислить минусы:
      1. Приложение на Электроне весит в 50 раз больше, чем аналогичное на нативном или кроссплатформенном компилируемом языке.
      2. Приложение на Электроне медленнее в несколько раз, чем приложение работающее нативно
      3. Приложение на Электроне напрямую зависит от возможностей "урезанного браузера", лежащего в основе приложения.
      4. Приложение на Электроне считаются мутабельными, а это запрещено для использования на мобильных площадках и принимаются приложения туда с трудом.
      5. Приложения не Электроне таскают с собой кучу одинаковых между такими же приложениями на Электроне файлов, которые даже не пытаются переиспользовать. В итоге полезная нагрузка приложения - это 10% от всего занимаемого места приложением. (Хотя Qt тоже этим немного грешит)


      1. johny_cat
        29.09.2021 16:56
        +1

        Как бы не хейтили электрон, у него НЕОЖИДАННО есть и плюсы

        По типу:

        1. Более простого входа в разработку (очень полезно для приложений, которым нужна простая смена дизайна, всё же html/css выучить проще чем тот же qt, а ещё если вспомнить туллинг имеющийся во фронте...)

        2. Не нужно задуматься на счёт кроссплатформенности (умные дядьки за тебя уже всё продумали)

        3. И возможно что-то ещё, что я сейчас не вспомнил

        Ну а по поводу минусов

        1. Ну тут всё верно, даже спорить не буду

        2. Вот тут не сказал бы, по больше части всё зависит от прямых рук разработчика приложения. В общем случае разница между скоростью работы не заметна на глаз. (Знаю я один пример, когда приложение на Java запускается дольше чем то, что было написано на Electron)

        3. И порой этого достаточно (честно даже не знаю что такого можно придумать, чтобы не хватило возможностей "урезанного браузера")

        4. Кто в здравом уме понесёт приложение на электроне на мобилки? (Да и где вы видели такое, чтоб электрон под мобилки собирался?)

        5. Это относится к первому недостатку, но и опять же, с ваших слов "Qt тоже этим немного грешит"

        Моё мнение такое, что для каждой технологии есть своё применение. Не думаю, что есть вообще какой либо смысл писать приложение на электроне, если оно задействует минимум его возможностей и/или его кодовая база в разы меньше общего бандла (код приложения + электрон). Вас в принципе засмеют (и правильно сделают), если вы будете писать какой-нибудь калькулятор на электроне, это глупо.

        В таком случае лучше писать на нативном или кроссплатформенном компилируемом языке. Да возможно дизайн приложения от этого пострадает + придётся повозиться с кросскомпиляцией (для нативки), но думаю оно того стоит.


        1. HemulGM
          29.09.2021 18:36

          Первый "плюс" разбивается о тот факт, что простой вход в Электрон доступен только веб-разработчикам. Новичкам (абсолютным) же будет только сложнее от архитектуры приложения и принципа разработки. JS, Node.js, NPM ...

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

          Тем не менее, основным минусом будет считаться производительность приложений и их глючность. Я бы не сказал, что Дискорд написан кривыми руками, тем не менее, проблем у Дискорда хватает. Скайп точно написан кривыми руками. Вероятно, хорошим примером против моего аргумента будет служить VS Code, который на стоке без кучи плагинов достаточно стабильно и шустро работает. Но, исключение из правил ...

          По поводу области применения. Если простые приложения на Электроне писать не стоит, а и крупные приложения (мощные редакторы графики или крупные CRM с сотнями окон) писать тоже не стоит, то в чем смысл? Его удел клиентские варианты веб-сайтов?

          Кроссплатформенность для инструментов для разработки десктоп софта это не редкость. Тот же Delphi позволяет создавать приложения с нативным кодом для всех платформ, включая и Android с iOS, в пару кликов переключая платформу для сборки. И визуальная составляющая даже близко уступать html/css не будет.


        1. borovinskiy
          13.11.2021 02:59

          Добавлю еще плюсов:
          4. Высокая скорость разработки сложных интерфейсов на реактивных фреймворках типа React, Vue, для которых также есть большое количество готовых компонент.
          5. Как следствие: экономия на создании и поддержке, да и программиста найти проще на JS.

          Есть богатый выбор из нативной разработки под каждую платформу отдельно, кроссплатформенной на фреймворках, React Native, Electron и куча другого ПО.

          Исходя из требований проекта и надо выбирать, что будет использоваться.