Первая программа 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 с результатом. |
contextBridge | API Electron: публикует объект в window renderer, не ломая изоляцию контекстов. |
contextIsolation: true | Renderer и preload в разных "мирах"; глобалы страницы не видят require напрямую. |
nodeIntegration: false | В renderer нет встроенного Node (require, fs) — только то, что разрешил preload. |
| Vite | Сборщик для React: быстрая пересборка при сохранении файла, в dev отдаёт UI на localhost:5173. |
Что получится
Окно с:
- Счётчиком — чистый React, данные только в renderer.
- Кнопкой "Открыть файл" — путь к файлу приходит из 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 handleOpen | invoke возвращает Promise — нужен await. |
await window.desktop.openFile() | Ждём ответ main; пользователь может нажать "Отмена" → null. |
if (path) setFilePath(path) | Обновляем UI только при реальном выборе. |
onClick={() => setCount((c) => c + 1)} | Функциональное обновление: актуальное значение из предыдущего рендера. |
Содержимое файла на диск пока не читаем — только путь. Следующий шаг в "Что попробовать" — fs.promises.readFile в main и второй канал IPC.
Запуск и отладка
| Команда / действие | Зачем |
|---|---|
npm run dev | Vite + Electron; в DevTools (Ctrl+Shift+I) видны ошибки React и сети. |
| DevTools → Console | Ошибки window.desktop is undefined видны сразу. |
| DevTools → Sources | Проверить, что preload подключился (вкладка preload иногда отдельно). |
Типичные сбои
| Симптом | Вероятная причина | Что проверить |
|---|---|---|
desktop is undefined | Preload не подключён или опечатка в 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 defined | Node в renderer вместо preload |
| IPC не отвечает | Разные имена канала в ipcMain.handle и invoke |
| Огромный установщик | Лишние пакеты в сборке |
Что попробовать дальше
- Читать файл в main:
fs.promises.readFile(path, 'utf8')внутриhandle, вернуть текст в UI вторым методомdesktop.readFile(path). - Меню приложения:
Menu.buildFromTemplateв main — пункты "Файл → Открыть" вызывают тот жеdialog. - Сборка установщика:
electron-builder— см. Electron. - Тяжёлые вычисления: не в UI-потоке renderer — Web Workers или задача в main с прогрессом через IPC.
Связанные материалы
- Electron — обзор
- Особенности десктопной разработки
- Первая программа на Node.js
- Первая программа на React
- React — компоненты-рецепты (Lab)
- Первая форма WPF — XAML, стили и шаблоны
Минимальная production-версия этого проекта
После первого прототипа полезно сразу перейти к "боевому минимуму", чтобы не накапливать технический долг.
Что добавить в ближайшей итерации
- Централизованный модуль каналов IPC (
shared/ipcChannels.js), чтобы убрать строковые дубли. - Обработку ошибок в каждом
invokeс человеко-понятными сообщениями в UI. - Логгер в main для операций с файлами и диалогами.
- Политику безопасности окна: CSP и ограничение внешней навигации.
- Базовый e2e smoke-тест запуска окна и вызова
openFile.
Пример расширения API preload
contextBridge.exposeInMainWorld('desktop', {
openFile: () => ipcRenderer.invoke('dialog:openFile'),
readText: (path) => ipcRenderer.invoke('fs:readText', path),
});
Идея простая: renderer знает только безопасные методы desktop.*, а детали файловой системы остаются в main.
Связь с другими практикумами
- WebView — когда нужен встроенный веб-контент без полного Electron-стека.
- Windows Forms (WinForms) — быстрый нативный .NET UI.
- Первая форма WPF — XAML, стили и шаблоны — альтернативный путь для XAML-стека.
Как развивать этот учебный проект
Итерация 1 — безопасность и контракт
- Вынесите имена IPC-каналов в единый модуль.
- Ограничьте API preload только необходимыми методами.
- Добавьте базовую валидацию входных параметров в
ipcMain.handle.
Итерация 2 — качество и сопровождение
- Добавьте e2e smoke-тест "запуск окна + openFile".
- Введите централизованный логгер в main-процессе.
- Подготовьте
dist-сборку и проверьте обновление с прошлой версии.
Этот путь превращает учебный пример в основу для командной разработки.
В подборках
Статья входит в тематические подборки и блок "С чего начать?" на главной. Соседние шаги того же маршрута:
Первые шаги (маршрут подборки) — Первая программа на Axum, Первая программа на React Native, Первая программа на Fiber, Первая программа на Expo, Первая программа на Echo, Первые шаги с SQL.