Первая программа Electron с React
Первая программа Electron с React
Electron и React - первое десктопное окно
Вы уже делали интерфейс в React в браузере и хотите то же самое в окне на рабочем столе — с доступом к файлам, системным диалогам и меню. Здесь собран первый рабочий проект: счётчик в React и кнопка «Открыть файл», которая вызывает стандартный диалог Windows/macOS/Linux.
Обзор процессов Electron — в Electron. API Node.js — 262. Общие правила десктопа (не блокировать UI) — 112.md.
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). Вы правите 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:
const { app, BrowserWindow, ipcMain, dialog } = require('electron');
const path = require('path');
function createWindow() {
const win = new BrowserWindow({
width: 900,
height: 600,
webPreferences: {
preload: path.join(__dirname, 'preload.js'),
contextIsolation: true,
nodeIntegration: false,
},
});
// dev: Vite на localhost:5173; production: file://...
if (process.env.VITE_DEV_SERVER_URL) {
win.loadURL(process.env.VITE_DEV_SERVER_URL);
} else {
win.loadFile(path.join(__dirname, '../dist/index.html'));
}
}
app.whenReady().then(createWindow);
ipcMain.handle('dialog:openFile', async () => {
const { canceled, filePaths } = await dialog.showOpenDialog({
properties: ['openFile'],
filters: [{ name: 'Text', extensions: ['txt', 'md'] }],
});
if (canceled || filePaths.length === 0) return null;
return filePaths[0];
});
Разбор по блокам
| Строки / блок | Смысл |
|---|---|
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:
import { useState } from 'react';
export default function App() {
const [count, setCount] = useState(0);
const [filePath, setFilePath] = useState('');
async function handleOpen() {
const path = await window.desktop.openFile();
if (path) setFilePath(path);
}
return (
<main style={{ fontFamily: 'system-ui', padding: 24 }}>
<h1>Electron + React</h1>
<p>Счётчик: {count}</p>
<button type="button" onClick={() => setCount((c) => c + 1)}>
+1
</button>
<button type="button" onClick={handleOpen} style={{ marginLeft: 8 }}>
Открыть файл
</button>
{filePath && <p>Файл: {filePath}</p>}
</main>
);
}
Разбор
| Фрагмент | Смысл |
|---|---|
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 — 114.md. |
Частые ошибки (сводка)
| Симптом | Причина |
|---|---|
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— см. 114.md. - Тяжёлые вычисления: не в UI-потоке renderer — Web Workers или задача в main с прогрессом через IPC.
Связанные материалы
- Electron — обзор
- Особенности десктопной разработки
- Первая программа на Node.js
- Первая программа на React
См. также
Другие статьи этого же раздела в боковом меню (как на странице «О разделе»). Настоятельно рекомендую ознакомиться со главой, посвящённой созданию десктопных приложений на Python - 5.02. Графика и игры. Десктопное приложение — это композитная сущность, объединяющая код, ресурсы, метаданные, конфигурации и, зачастую, механизмы обновления, диагностики и интеграции с другими компонентами системы. Многопоточность, реактивность, ресурсы, отладка и прочее. WebView - встроенный браузер в приложениях. Electron — десктопные приложения на HTML, CSS и JavaScript с процессами main, preload и renderer. Windows Forms — платформа GUI для классических настольных приложений Windows на .NET; формы, контролы, события, привязка данных и визуальный конструктор Visual Studio. Платформа разработчика Windows — Windows SDK, Windows App SDK, WinUI 3, WPF, среда разработки, поддержка и обзор драйверов по документации Microsoft. Учётная запись разработчика, MSIX, Partner Center, сертификация и распространение приложений для Windows через Microsoft Store. Работа с графовыми структурами в коде - визуализация состояний узлов и отладка обходов графа на практике. Краткие итоги раздела «Десктопные приложения». Итоги раздела Десктопные приложения — вопросы для самопроверки в энциклопедии Вселенная IT.Архитектура десктопных приложений
Разработка приложений для настольных операционных систем
Особенности разработки десктопных приложений
WebView
Electron
Windows Forms (WinForms)
Разработка приложений для Windows (Microsoft Learn)
Microsoft Store и публикация Windows-приложений
Работа с графовыми структурами в коде
Итоги
Чек-лист самопроверки