Перейти к основному содержанию

Моделирование процесса

Electron наследует концепцию множественных процессов от Chromium, что делает фреймворк похожим по архитектуре на современный веб-браузер. Это руководство расширяет понятия, приведенные в Учебном материале.

Почему недостаточно одного процесса?

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

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

Модель множественных процессов

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

Chrome's multi-process architecture

Приложения работающие на Electron устроены примерно так же. Как разработчик, вы управляете двумя типами процессов: main и renderer. Они аналогичны вышеописанным процессу браузера и отображения в Chrome.

Основной процесс (main)

Каждое приложение Electron имеет один основной процесс, который является точкой входа в приложение. Основной процесс работает в среде Node.js, то есть имеет возможность импортировать (require) модули и использовать весь функционал API Node.js.

Управление окнами

Основными задачами главного процесса являются создание и управление окнами приложений с помощью модуля BrowserWindow.

Каждый экземпляр класса BrowserWindow создает окно приложения, которое загружает веб-страницу в отдельном процессе рендеринга. Взаимодействовать с содержимым веб-страницы можно из основного процесса, используя объект webContents окна.

main.js
const { BrowserWindow } = require('electron')

const win = new BrowserWindow({ width: 800, height: 1500 })
win.loadURL('https://github.com')

const contents = win.webContents
console.log(contents)

Примечание: Процесс рендеринга создается также для встраиваемого веб-содержимого, например, модуля BrowserView. Объект webContents также доступен и для такого содержимого.

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

При завершении экземпляра BrowserWindow завершается и соответствующий ему процесс рендера.

Жизненный цикл приложения

Основной процесс также управляет жизненным циклом приложения посредством модуля Electron app. This module provides a large set of events and methods that you can use to add custom application behavior (for instance, programmatically quitting your application, modifying the application dock, or showing an About panel).

В качестве примера можно привести приложение, показанное в кратком стартовом руководстве, которое использует API app для создания более естественного пользовательского UX.

main.js
// Завершение работы приложения, когда все окна закрыты, для платформ отличных от macOS
app.on('window-all-closed', () => {
if (process.platform !== 'darwin') app.quit()
})

Native APIs

Чтобы расширить возможности Electron, не ограничиваясь оболочкой Chromium для веб-содержимого, основной процесс также добавляет кастомные API для взаимодействия с операционной системой пользователя. Electron предоставляет различные модули, управляющие встроенными возможностями рабочего стола, такими как меню, диалоговые окна и значки в трее.

Полный список модулей главного процесса Electron приведен в документации по API.

Процесс отображения (renderer)

Каждое приложение Electron для каждого открытого окна BrowserWindow (и каждого встраиваемого веб-содержимого) создает отдельный процесс рендеринга (renderer). Как следует из названия, renderer отвечает за отрисовку (рендер) контента. По идее, код, выполняемый в процессах рендеринга, работает в соответствии с веб-стандартами (по крайней мере, в рамках Chromium).

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

Хотя объяснение каждой спецификации не входит в рамки данного руководства, необходимо знать минимум:

  • HTML-файл - это точка входа в процесс отображения (renderer).
  • Стилизация пользовательского интерфейса добавляется с помощью каскадных таблиц (CSS).
  • Исполняемый код JavaScript может быть добавлен через тэг <script>.

Это также означает, что renderer не имеет прямого доступа к require и другим API Node.js. Для того чтобы непосредственно подключать модули NPM к рендереру, необходимо использовать те же цепочки пакетов (например: webpack, parcel, rollup, esbuild, vite и т. д.), которые применяются в Web.

Предупреждение

Для удобства разработки процессы рендерера могут быть запущены с доступом к окружению Node.js. В прошлом эта конфигурация использовалась по умолчанию, но по соображениям безопасности она была отключена.

На этом этапе вы можете задаться вопросом, как пользовательские интерфейсы процессов отрисовки могут взаимодействовать с Node.js и встроенными функциями рабочего стола Electron, если эти функции доступны только из главного процесса. На самом деле не существует способа непосредственного импорта скриптов Electron.

Preload scripts

Preload scripts contain code that executes in a renderer process before its web content begins loading. These scripts run within the renderer context, but are granted more privileges by having access to Node.js APIs.

A preload script can be attached to the main process in the BrowserWindow constructor's webPreferences option.

main.js
const { BrowserWindow } = require('electron')
// ...
const win = new BrowserWindow({
webPreferences: {
preload: 'path/to/preload.js'
}
})
// ...

Because the preload script shares a global Window interface with the renderers and can access Node.js APIs, it serves to enhance your renderer by exposing arbitrary APIs in the window global that your web contents can then consume.

Although preload scripts share a window global with the renderer they're attached to, you cannot directly attach any variables from the preload script to window because of the contextIsolation default.

preload.js
window.myAPI = {
desktop: true
}
renderer.js
console.log(window.myAPI)
// => undefined

Context Isolation means that preload scripts are isolated from the renderer's main world to avoid leaking any privileged APIs into your web content's code.

Instead, use the contextBridge module to accomplish this securely:

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

contextBridge.exposeInMainWorld('myAPI', {
desktop: true
})
renderer.js
console.log(window.myAPI)
// => { desktop: true }

This feature is incredibly useful for two main purposes:

  • By exposing ipcRenderer helpers to the renderer, you can use inter-process communication (IPC) to trigger main process tasks from the renderer (and vice-versa).
  • If you're developing an Electron wrapper for an existing web app hosted on a remote URL, you can add custom properties onto the renderer's window global that can be used for desktop-only logic on the web client's side.

The utility process

Each Electron app can spawn multiple child processes from the main process using the UtilityProcess API. The utility process runs in a Node.js environment, meaning it has the ability to require modules and use all of Node.js APIs. The utility process can be used to host for example: untrusted services, CPU intensive tasks or crash prone components which would have previously been hosted in the main process or process spawned with Node.js child_process.fork API. The primary difference between the utility process and process spawned by Node.js child_process module is that the utility process can establish a communication channel with a renderer process using MessagePorts. An Electron app can always prefer the UtilityProcess API over Node.js child_process.fork API when there is need to fork a child process from the main process.

Process-specific module aliases (TypeScript)

Electron's npm package also exports subpaths that contain a subset of Electron's TypeScript type definitions.

  • electron/main includes types for all main process modules.
  • electron/renderer includes types for all renderer process modules.
  • electron/common includes types for modules that can run in main and renderer processes.

These aliases have no impact on runtime, but can be used for typechecking and autocomplete.

Usage example
const { app } = require('electron/main')
const { shell } = require('electron/common')