️❗ Проверьте этот пост в блоге на mmazzarolo.com для выделенных блоков кода.

TL;DR: пошаговое руководство, объясняющее, как создать настольное приложение с помощью Create React App (CRA) и Electron. Исходный код проекта вы можете найти на GitHub.

Недавно мне нужно было обернуть приложение React, сгенерированное с помощью Create React App (CRA), с помощью Electron (ну, само приложение использует React Native for Web, но это не имеет значения).
Моя цель заключалась в том, чтобы максимально оставаться в пределах ограничений приложения Create React (без извлечения).
В Интернете нет недостатка в руководствах о том, как это сделать. Тем не менее, я не смог найти тот, который полностью соответствует рекомендациям по безопасности Electron и обеспечивает настройку дистрибутива с использованием Electron-builder.
Итак, вот еще один учебник о том, как обернуть приложение, созданное с помощью Create React App в Electron. — от первоначальных строительных лесов до рабочего процесса дистрибуции.

Скаффолдинг приложения React

Начнем с пустого приложения React, созданного с помощью Create React App.

npx create-react-app my-electron-app

Затем добавьте следующие зависимости (большинство из них здесь только для упрощения процесса разработки):

cd my-electron-app yarn add -D concurrently cross-env electron electron-builder electronmon wait-on
  • concurrently: Одновременное выполнение нескольких команд. Мы будем использовать его для запуска процесса Electron и приложения React в режиме просмотра.
  • cross-env: Запускайте сценарии, устанавливающие и использующие переменные среды на разных платформах. Мы будем использовать его, чтобы сделать наши скрипты совместимыми как с операционными системами Unix, так и с Windows.
  • electron: основная структура для создания приложения.
  • electron-builder: Комплексное решение для упаковки и сборки готового к распространению приложения Electron для macOS, Windows и Linux.
  • electronmon: Вроде , но для процесса Electron. Позволяет просматривать и перезагружать наше приложение Electron.
  • wait-on: Утилита для ожидания файлов, портов, сокетов и т. д. Мы будем использовать ее для ожидания сборки приложения React, прежде чем открывать приложение Electron (во время разработки).

Основной скрипт Electron

Следующий шаг — создание основного скрипта Electron. Этот скрипт управляет основным процессом, который работает в полной среде Node.js и отвечает за управление жизненным циклом вашего приложения, отображение собственных интерфейсов, выполнение привилегированных операций и управление процессами рендеринга.

Основной скрипт Electron часто называется main.js и хранится в <project-root>/electron/main.js, но в нашем случае мы назовем его electron.js (чтобы устранить неоднозначность) и сохраним его в <project-root>/public/electron.js (чтобы приложение Create React автоматически скопировало его в каталог сборки).

// public/electron.js

// Module to control the application lifecycle and the native browser window.
const { app, BrowserWindow, protocol } = require("electron");
const path = require("path");
const url = require("url");

// Create the native browser window.
function createWindow() {
  const mainWindow = new BrowserWindow({
    width: 800,
    height: 600,
    // Set the path of an additional "preload" script that can be used to
    // communicate between node-land and browser-land.
    webPreferences: {
      preload: path.join(__dirname, "preload.js"),
    },
  });

  // In production, set the initial browser path to the local bundle generated
  // by the Create React App build process.
  // In development, set it to localhost to allow live/hot-reloading.
  const appURL = app.isPackaged
    ? url.format({
        pathname: path.join(__dirname, "index.html"),
        protocol: "file:",
        slashes: true,
      })
    : "http://localhost:3000";
  mainWindow.loadURL(appURL);

  // Automatically open Chrome's DevTools in development mode.
  if (!app.isPackaged) {
    mainWindow.webContents.openDevTools();
  }
}

// Setup a local proxy to adjust the paths of requested files when loading
// them from the local production bundle (e.g.: local fonts, etc...).
function setupLocalFilesNormalizerProxy() {
  protocol.registerHttpProtocol(
    "file",
    (request, callback) => {
      const url = request.url.substr(8);
      callback({ path: path.normalize(`${__dirname}/${url}`) });
    },
    (error) => {
      if (error) console.error("Failed to register protocol");
    }
  );
}

// This method will be called when Electron has finished its initialization and
// is ready to create the browser windows.
// Some APIs can only be used after this event occurs.
app.whenReady().then(() => {
  createWindow();
  setupLocalFilesNormalizerProxy();

  app.on("activate", function () {
    // On macOS it's common to re-create a window in the app when the
    // dock icon is clicked and there are no other windows open.
    if (BrowserWindow.getAllWindows().length === 0) {
      createWindow();
    }
  });
});

// Quit when all windows are closed, except on macOS.
// There, it's common for applications and their menu bar to stay active until
// the user quits explicitly with Cmd + Q.
app.on("window-all-closed", function () {
  if (process.platform !== "darwin") {
    app.quit();
  }
});

// If your app has no need to navigate or only needs to navigate to known pages,
// it is a good idea to limit navigation outright to that known scope,
// disallowing any other kinds of navigation.
const allowedNavigationDestinations = "https://my-electron-app.com";
app.on("web-contents-created", (event, contents) => {
  contents.on("will-navigate", (event, navigationUrl) => {
    const parsedUrl = new URL(navigationUrl);

    if (!allowedNavigationDestinations.includes(parsedUrl.origin)) {
      event.preventDefault();
    }
  });
});

// In this file you can include the rest of your app's specific main process
// code. You can also put them in separate files and require them here.

Да, это не минимальная установка electron.js, но я хотел получить несколько приятных значений по умолчанию и убедился, что мы следуем рекомендациям по безопасности Electron.

Во время выполнения Electron будет искать этот скрипт в поле main конфигурации приложения package.json, так что давайте обновим его:

{
  "name": "my-electron-app",
  "version": "0.1.0",
  "private": true,
+ "main": "./public/electron.js",
  "dependencies": {

Скрипт предварительной загрузки Electron

По умолчанию процесс, запущенный в вашем браузере, не сможет взаимодействовать с процессом Node.js. Electron решает эту проблему, позволяя использовать сценарий предварительной загрузки: сценарий, который запускается до загрузки процесса средства визуализации и имеет доступ как к глобальным переменным средства визуализации (например, window и document), так и к среде Node.js.

В нашем сценарии electron.js мы уже указали, что ожидаем загрузки сценария предварительной загрузки из <project-root>/public/preload.js. Итак, давайте создадим его:

// public/preload.js

// All of the Node.js APIs are available in the preload process.
// It has the same sandbox as a Chrome extension.
const { contextBridge } = require("electron");

// As an example, here we use the exposeInMainWorld API to expose the browsers
// and node versions to the main window.
// They'll be accessible at "window.versions".
process.once("loaded", () => {
  contextBridge.exposeInMainWorld("versions", process.versions);
});

Приведенный выше код обращается к объекту Node.js process.versions и предоставляет его в приложении React, делая его доступным по адресу window.versions.

Создание приложения Create React, совместимого с Electron

Наша цель — оставаться в экосистеме Create React App без извлечения и использовать Electron только для рендеринга приложения React.
Для этого необходимо несколько настроек.

Обновите свойство homepage

Нам нужно принудительно создать приложение React для определения относительного корневого пути в сгенерированном HTML-файле. Это требование, потому что мы не собираемся обслуживать HTML-файл; он будет загружен непосредственно Электроном. Для этого мы можем установить для свойства homepage элемента package.json значение ./ (дополнительную информацию см. в разделе Построение относительных путей в документации по созданию приложения React).

{
  "name": "my-electron-app",
  "version": "0.1.0",
  "private": true,
+ "homepage": "./",
  "main": "./public/electron.js",
  "dependencies": {

Обновить цели browserslist

Обновите раздел browserslist в package.json, чтобы он поддерживал только последнюю версию Electron. Это гарантирует, что Webpack/Babel добавят только те полифилы и функции, которые нам строго необходимы, сведя размер пакета к минимуму.

"browserslist": {
    "production": [
+     "last 1 electron version",
-      ">0.2%",
-     "not dead",
-     "not op_mini all"
    ],
    "development": [
+     "last 1 electron version",
-     "last 1 chrome version",
-     "last 1 firefox version",
-     "last 1 safari version"
    ]
  },

Определить политику безопасности контента

Политика безопасности контента (CSP) — это дополнительный уровень защиты от атак с использованием межсайтовых сценариев и атак с внедрением данных. Поэтому я настоятельно рекомендую включить его в <project-root>/public/index.html.
Следующий CSP позволит Electron запускать только встроенные скрипты (те, которые вводятся в файл HTML в процессе сборки приложения Create React).

    <meta name="theme-color" content="#000000" />
    <meta
      name="description"
      content="Web site created using create-react-app"
    />
+   <meta
+     http-equiv="Content-Security-Policy"
+     content="script-src 'self' 'unsafe-inline';"
+   />

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

Определить сценарий запуска/разработки

В своем package.json определите скрипт для создания приложения Create React и запустите процесс Electron в режиме наблюдения:

"scripts": {
    "start": "react-scripts start",
    "build": "react-scripts build",
    "test": "react-scripts test",
    "eject": "react-scripts eject",
+   "electron:start": "concurrently -k \"cross-env BROWSER=none yarn start\" \"wait-on http://localhost:3000 && electronmon .\""
  },

Вот разбивка того, что он делает:

  • concurrently -k вызывает последующие команды параллельно и уничтожает их обе, когда процесс останавливается.
  • cross-env BROWSER=none yarn start устанавливает переменные среды BROWSER=none (используя cross-env для совместимости с Windows), чтобы отключить автоматическое открытие браузера, и вызывает скрипт start, который запускает сборку Create React App в режиме наблюдения.
  • wait-on http://localhost:3000 && electronmon . ждет, пока сервер разработки Create React App обслужит приложение на локальном хосте: 3000, а затем вызывает electronmon ., чтобы запустить дополнение Electron в режиме наблюдения.

Теперь вы можете запустить yarn electron:start, чтобы запустить приложение React в Electron, а не в окне браузера.

Упакуйте приложение Electron для распространения

Наконец, нам нужно внести несколько незначительных изменений в настройку Create React App, чтобы сгенерировать дистрибутивы для конкретной платформы, чтобы можно было установить наше приложение. Мы будем использовать Electron-builder, основанное на конфигурации решение для упаковки и сборки готовых к распространению приложений Electron для macOS, Windows и Linux.

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

Установите автора приложения и описание

Electron-builder выводит некоторую информацию по умолчанию, необходимую для сборки распространяемого файла (имя приложения, автора и описание) из package.json, поэтому давайте укажем их:

  "name": "my-electron-app",
  "version": "0.1.0",
  "private": true,
+ "author": "John Doe",
+ "description": "My fantastic Electron app",
  "homepage": "./",
  "main": "./public/electron.js",
  "dependencies": {

Установите конфигурацию сборки

Добавим минимальную конфигурацию сборщика электронов в package.json с помощью клавиши build на верхнем уровне:

  • appId: Идентификатор приложения, используемый для идентификации приложения в macOS (как CFBundleIdentifier) и Windows (как Идентификатор модели пользователя приложения).
  • productName: имя приложения, как показано в исполняемом файле приложения.
  • directories.buildResources: Путь к корневому каталогу, в котором хранятся ресурсы, не упакованные в приложение.
  • files: Общие дополнительные файлы (за пределами directories.buildResources), необходимые приложению для запуска.
  • mac, win, linux: Конфигурации для конкретных платформ.
+ "build": {
+   "appId": "com.electron.myapp",
+   "productName": "My Electron App",
+   "files": ["build/ **/*", "node_modules/** /*"],
+   "directories": {
+     "buildResources": "public"
+   },
+   "mac": {
+     "target": "dmg"
+   },
+   "win": {
+     "target": "nsis"
+   },
+   "linux": {
+     "target": "deb"
+   }
+ }

Добавить значок приложения

По умолчанию Electron-Builder будет искать значок приложения в каталоге <root-project>/build/icon.png, так что вам должно быть удобно, если вы поместите его в каталог public (процесс создания приложения React React позаботится о его перемещении в каталог build).

Дополнительную информацию см. в Документации по иконкам Electronic-Builder.

Добавьте скрипты упаковки

Наконец, чтобы Electron-builder упаковал наше приложение, мы можем добавить скрипт упаковки для каждой целевой платформы в package.json:

"scripts": {
    "start": "react-scripts start",
    "build": "react-scripts build",
    "test": "react-scripts test",
    "eject": "react-scripts eject",
    "electron:start": "concurrently -k \"cross-env BROWSER=none yarn start\" \"wait-on http://localhost:3000 && electronmon .\"",
+   "electron:package:mac": "yarn build && electron-builder -m -c.extraMetadata.main=build/electron.js",
+   "electron:package:win": "yarn build && electron-builder -w -c.extraMetadata.main=build/electron.js",
+   "electron:package:linux": "yarn build && electron-builder -l -c.extraMetadata.main=build/electron.js"
  },

Эти команды создадут производственный пакет приложения React и упакуют его в дистрибутивы для Windows, macOS и Linux соответственно. По умолчанию дистрибутивы будут иметь форму NSIS (Windows), dmg (macOS) и deb (Linux).

Сгенерированные дистрибутивные файлы будут размещены в <project-root>/dist, поэтому обязательно добавьте этот каталог в .gitignore:

+ /dist

Резюме

Вот и все.
Теперь вы можете запустить yarn electron:start, чтобы запустить процесс разработки, и yarn electron:package:<platform>, чтобы создать распространяемый пакет.

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

Вы можете найти полный код для этого поста в блоге на GitHub.

Первоначально опубликовано на https://mmazzarolo.com.