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

Первая программа Electron с React

Разработчику Инженеру

Темп прохождения практикума

Двигайтесь короткими циклами — собрали шаг, запустили, проверили поведение, только потом переходите дальше.
Для новичка это самый надежный способ избежать "кома" из ошибок в main, preload и renderer одновременно.

Теория Electron — Electron; общая архитектура десктопа — Архитектура десктопных приложений. База по React — 272 и галерея компонентов.


Первая программа Electron с React

Electron и React - первое десктопное окно

Вы уже делали интерфейс в React в браузере и хотите то же самое в окне на рабочем столе — с доступом к файлам, системным диалогам и меню. Здесь собран первый рабочий проект: счётчик в React и кнопка "Открыть файл", которая вызывает стандартный диалог Windows/macOS/Linux.

Обзор процессов Electron — в Electron. API Node.js — Первая программа на Node.js. Общие правила десктопа (не блокировать UI) — Особенности разработки десктопных приложений.


Electron и React — десктоп на веб-стеке

Electron — это способ упаковать веб-интерфейс (HTML, CSS, JavaScript) в отдельное приложение с окном, как VS Code или Discord. Внутри — движок Chromium (рисует страницу) и Node.js (читает файлы, показывает диалоги). Вы пишете UI на React, а "системные" действия — в main-процессе.

Схема в одном предложении:

ЧастьРольАналогия
RendererТо, что видит пользователь (React, кнопки)Витрина магазина
MainОкна, файлы, меню, диалогиОфис с ключами от склада
PreloadУзкий мост: только разрешённые вызовы из UI в mainОхрана на входе со списком "можно / нельзя"
Пользователь нажимает кнопку в React (renderer)


window.desktop.openFile() ← preload выставил эту функцию


ipcRenderer.invoke('dialog:openFile') ← сообщение в main


ipcMain.handle(...) → dialog.showOpenDialog() ← ОС показывает окно выбора файла


Путь к файлу возвращается в React и показывается на экране

React рисует кнопки, main открывает файл, preload соединяет их безопасно. Прямой доступ к диску из UI в production обычно отключают — иначе уязвимость в странице превратится в чтение любых файлов на компьютере.


Словарь терминов (прочитайте до кода)

ТерминЧто это простыми словами
Main processГлавный процесс Node.js: создаёт окна, живёт всё время работы приложения. Один на приложение.
Renderer processПроцесс "вкладки": внутри него крутится ваша React-страница. Окно = отдельный renderer (часто).
PreloadСкрипт, который выполняется до загрузки страницы и может безопасно передать в window только нужные функции.
IPC (Inter-Process Communication)Обмен сообщениями между main и renderer. В коде: ipcMain / ipcRenderer.
invoke / handleПара "запрос — ответ": UI вызывает invoke, main обрабатывает в handle и возвращает Promise с результатом.
contextBridgeAPI Electron: публикует объект в window renderer, не ломая изоляцию контекстов.
contextIsolation: trueRenderer и preload в разных "мирах"; глобалы страницы не видят require напрямую.
nodeIntegration: falseВ renderer нет встроенного Node (require, fs) — только то, что разрешил preload.
ViteСборщик для React: быстрая пересборка при сохранении файла, в dev отдаёт UI на localhost:5173.

Что получится

Окно с:

  1. Счётчиком — чистый React, данные только в renderer.
  2. Кнопкой "Открыть файл" — путь к файлу приходит из main через ipcRenderer.invoke.

Так вы увидите оба слоя: UI без привилегий ОС и main с доступом к диалогам.


Создание проекта

Рекомендуемый старт — шаблон с Vite (удобно для React: горячая перезагрузка UI):

npm create @quick-start/electron@latest my-app -- --template react
cd my-app
npm install
npm run dev

Что делает npm run dev: обычно одновременно поднимает Vite (страница React) и Electron (окно, которое грузит этот URL). Общая схема dev-сервера — Запуск и перезапуск приложений. Вы правите src/App.jsx — окно обновляется без полной пересборки.

Если мастер недоступен — минимальная структура вручную:

my-app/
package.json # точка входа: "main": "electron/main.js"
electron/
main.js # main process
preload.js # мост в renderer
src/ # React (Vite)
main.jsx
App.jsx
index.html

В package.json укажите "main": "electron/main.js" и скрипты dev / start по README шаблона. Поле "main" говорит Electron, какой файл запустить первым — это всегда main-процесс, не React.


Main process — окно и IPC

Файл electron/main.js:

Код ITЗагрузка примера кода…


Разбор по блокам

Строки / блокСмысл
require('electron')В main доступен полный Node и модули Electron.
appЖизненный цикл приложения: готовность, выход, активация на macOS.
BrowserWindowСоздаёт окно ОС с веб-содержимым внутри.
webPreferences.preloadПуть к preload; выполняется до React и подключает мост.
contextIsolation: trueБезопасная изоляция: страница не подменяет внутренности Electron.
nodeIntegration: falseВ renderer нет require('fs') — только IPC через preload.
VITE_DEV_SERVER_URLВ разработке грузим React с Vite; в production — собранный dist/index.html.
ipcMain.handle('dialog:openFile', ...)Регистрируем обработчик: renderer вызовет канал с тем же именем.
dialog.showOpenDialogНативный диалог выбора файла; возвращает canceled и массив filePaths.
return filePaths[0]Значение уйдёт в Promise на стороне React (await openFile()).

Почему nodeIntegration: false: renderer — по сути веб-страница. Прямой require('fs') в UI даёт полный доступ к диску любому скрипту на странице. Всё опасное остаётся в main; в UI — узкий мост preload.

Имя канала 'dialog:openFile'договорённость: в preload должно быть invoke('dialog:openFile') с тем же строковым литералом.


Preload — мост в React

Файл electron/preload.js:

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

contextBridge.exposeInMainWorld('desktop', {
openFile: () => ipcRenderer.invoke('dialog:openFile'),
});

Разбор

ЭлементСмысл
contextBridgeОфициальный способ "пробросить" API в window renderer.
exposeInMainWorld('desktop', { ... })В React появится window.desktop.
openFileОдна разрешённая операция — открыть диалог файла.
ipcRenderer.invoke(...)Асинхронный запрос в main; результат — Promise (путь или null).

В React вызываете window.desktop.openFile(). Для TypeScript добавьте global.d.ts:

interface DesktopAPI {
openFile: () => Promise<string | null>;
}
interface Window {
desktop: DesktopAPI;
}

Без этой декларации TypeScript будет ругаться на window.desktop.


React — интерфейс

Файл src/App.jsx:

Код ITЗагрузка примера кода…


Разбор

ФрагментСмысл
useState(0)Локальное состояние счётчика; main не участвует.
useState('')Строка пути после выбора файла.
async function handleOpeninvoke возвращает Promise — нужен await.
await window.desktop.openFile()Ждём ответ main; пользователь может нажать "Отмена" → null.
if (path) setFilePath(path)Обновляем UI только при реальном выборе.
onClick=&#123;() => setCount((c) => c + 1)&#125;Функциональное обновление: актуальное значение из предыдущего рендера.

Содержимое файла на диск пока не читаем — только путь. Следующий шаг в "Что попробовать" — fs.promises.readFile в main и второй канал IPC.


Запуск и отладка

Команда / действиеЗачем
npm run devVite + Electron; в DevTools (Ctrl+Shift+I) видны ошибки React и сети.
DevTools → ConsoleОшибки window.desktop is undefined видны сразу.
DevTools → SourcesПроверить, что preload подключился (вкладка preload иногда отдельно).

Типичные сбои

СимптомВероятная причинаЧто проверить
desktop is undefinedPreload не подключён или опечатка в exposeInMainWorldПуть в preload: path.join(__dirname, 'preload.js'), имя 'desktop'.
IPC не отвечает / зависаетРазные имена каналаСтрока в handle и в invoke должна совпадать побайтно.
Белый экранНеверный URL Vite или пустой distВ dev — работает ли http://localhost:5173 в браузере; в prod — npm run build перед electron ..
require is not definedВключили nodeIntegration: true в renderer и пишут Node в ReactВерните false, перенесите логику в main + preload.
Огромный установщикВ bundle попали devDependenciesНастройте files в electron-builder — Electron.

Частые ошибки

СимптомПричина
require is not definedNode в renderer вместо preload
IPC не отвечаетРазные имена канала в ipcMain.handle и invoke
Огромный установщикЛишние пакеты в сборке

Что попробовать дальше

  1. Читать файл в main: fs.promises.readFile(path, 'utf8') внутри handle, вернуть текст в UI вторым методом desktop.readFile(path).
  2. Меню приложения: Menu.buildFromTemplate в main — пункты "Файл → Открыть" вызывают тот же dialog.
  3. Сборка установщика: electron-builder — см. Electron.
  4. Тяжёлые вычисления: не в UI-потоке renderer — Web Workers или задача в main с прогрессом через IPC.

Связанные материалы


Минимальная production-версия этого проекта

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


Что добавить в ближайшей итерации

  1. Централизованный модуль каналов IPC (shared/ipcChannels.js), чтобы убрать строковые дубли.
  2. Обработку ошибок в каждом invoke с человеко-понятными сообщениями в UI.
  3. Логгер в main для операций с файлами и диалогами.
  4. Политику безопасности окна: CSP и ограничение внешней навигации.
  5. Базовый e2e smoke-тест запуска окна и вызова openFile.

Пример расширения API preload

contextBridge.exposeInMainWorld('desktop', {
openFile: () => ipcRenderer.invoke('dialog:openFile'),
readText: (path) => ipcRenderer.invoke('fs:readText', path),
});

Идея простая: renderer знает только безопасные методы desktop.*, а детали файловой системы остаются в main.


Связь с другими практикумами


Как развивать этот учебный проект

Итерация 1 — безопасность и контракт

  1. Вынесите имена IPC-каналов в единый модуль.
  2. Ограничьте API preload только необходимыми методами.
  3. Добавьте базовую валидацию входных параметров в ipcMain.handle.

Итерация 2 — качество и сопровождение

  1. Добавьте e2e smoke-тест "запуск окна + openFile".
  2. Введите централизованный логгер в main-процессе.
  3. Подготовьте dist-сборку и проверьте обновление с прошлой версии.

Этот путь превращает учебный пример в основу для командной разработки.


В подборках

Статья входит в тематические подборки и блок "С чего начать?" на главной. Соседние шаги того же маршрута:

Первые шаги (маршрут подборки) — Первая программа на Axum, Первая программа на React Native, Первая программа на Fiber, Первая программа на Expo, Первая программа на Echo, Первые шаги с SQL.