Реализация межпроцессного взаимодействия (IPC) в Electron-приложении
IPC (Inter-Process Communication) в Electron — это единственный безопасный способ дать renderer-процессу доступ к системным ресурсам. Renderer работает в изолированном Chromium-контексте; всё, что связано с файловой системой, нативными API и Node.js — только через IPC и preload script.
Архитектура безопасного IPC
Renderer (Chromium)
↓ window.electronAPI.someMethod()
Preload Script (contextBridge)
↓ ipcRenderer.invoke('channel', payload)
Main Process (Node.js)
↓ ipcMain.handle('channel', handler)
Preload script — граница безопасности. Он экспонирует в renderer ровно то API, которое нужно, и ничего лишнего.
Preload: типизированный мост
// main/preload.ts
import { contextBridge, ipcRenderer, IpcRendererEvent } from 'electron';
// Типы для статического анализа
type FileInfo = { path: string; content: string; size: number };
type UpdateInfo = { version: string; releaseNotes?: string };
const api = {
// invoke — запрос с ответом (аналог HTTP request/response)
fs: {
readFile: (path: string): Promise<FileInfo> =>
ipcRenderer.invoke('fs:readFile', path),
writeFile: (path: string, content: string): Promise<void> =>
ipcRenderer.invoke('fs:writeFile', path, content),
showOpenDialog: (options: Electron.OpenDialogOptions): Promise<string[] | null> =>
ipcRenderer.invoke('dialog:showOpen', options),
showSaveDialog: (options: Electron.SaveDialogOptions): Promise<string | null> =>
ipcRenderer.invoke('dialog:showSave', options),
},
app: {
getVersion: (): Promise<string> =>
ipcRenderer.invoke('app:getVersion'),
getPath: (name: string): Promise<string> =>
ipcRenderer.invoke('app:getPath', name),
},
window: {
// send — одностороннее сообщение без ответа
minimize: () => ipcRenderer.send('window:minimize'),
maximize: () => ipcRenderer.send('window:maximize'),
close: () => ipcRenderer.send('window:close'),
setTitle: (title: string) => ipcRenderer.send('window:setTitle', title),
},
// Подписка на события из main процесса
on: {
updateAvailable: (callback: (info: UpdateInfo) => void) => {
const listener = (_: IpcRendererEvent, info: UpdateInfo) => callback(info);
ipcRenderer.on('update:available', listener);
// Возвращаем функцию для отписки
return () => ipcRenderer.removeListener('update:available', listener);
},
networkChange: (callback: (isOnline: boolean) => void) => {
const listener = (_: IpcRendererEvent, isOnline: boolean) => callback(isOnline);
ipcRenderer.on('network:change', listener);
return () => ipcRenderer.removeListener('network:change', listener);
},
}
};
contextBridge.exposeInMainWorld('electronAPI', api);
// Тип для использования в renderer
export type ElectronAPI = typeof api;
Типизация в renderer
// renderer/types/electron.d.ts
import type { ElectronAPI } from '../../main/preload';
declare global {
interface Window {
electronAPI: ElectronAPI;
}
}
Main: обработчики IPC
// main/ipc-handlers.js
const { ipcMain, app, dialog, BrowserWindow } = require('electron');
const fs = require('fs/promises');
const path = require('path');
function registerHandlers() {
// handle — для invoke (с ответом)
ipcMain.handle('fs:readFile', async (event, filePath) => {
// Валидация — renderer не должен читать произвольные файлы
const resolvedPath = path.resolve(filePath);
const allowedDirs = [
app.getPath('documents'),
app.getPath('downloads'),
app.getPath('userData')
];
const isAllowed = allowedDirs.some(dir => resolvedPath.startsWith(dir));
if (!isAllowed) {
throw new Error(`Access denied: ${resolvedPath}`);
}
const content = await fs.readFile(resolvedPath, 'utf-8');
const stat = await fs.stat(resolvedPath);
return {
path: resolvedPath,
content,
size: stat.size
};
});
ipcMain.handle('fs:writeFile', async (event, filePath, content) => {
await fs.writeFile(filePath, content, 'utf-8');
});
ipcMain.handle('dialog:showOpen', async (event, options) => {
const win = BrowserWindow.fromWebContents(event.sender);
const result = await dialog.showOpenDialog(win, options);
return result.canceled ? null : result.filePaths;
});
ipcMain.handle('dialog:showSave', async (event, options) => {
const win = BrowserWindow.fromWebContents(event.sender);
const result = await dialog.showSaveDialog(win, options);
return result.canceled ? null : result.filePath;
});
ipcMain.handle('app:getVersion', () => app.getVersion());
ipcMain.handle('app:getPath', (event, name) => {
const allowed = ['home', 'appData', 'userData', 'documents', 'downloads', 'temp'];
if (!allowed.includes(name)) throw new Error(`Path not allowed: ${name}`);
return app.getPath(name);
});
// on — для send (без ответа)
ipcMain.on('window:minimize', (event) => {
BrowserWindow.fromWebContents(event.sender)?.minimize();
});
ipcMain.on('window:maximize', (event) => {
const win = BrowserWindow.fromWebContents(event.sender);
win?.isMaximized() ? win.unmaximize() : win?.maximize();
});
ipcMain.on('window:close', (event) => {
BrowserWindow.fromWebContents(event.sender)?.close();
});
ipcMain.on('window:setTitle', (event, title) => {
BrowserWindow.fromWebContents(event.sender)?.setTitle(title);
});
}
module.exports = { registerHandlers };
Стриминг данных через IPC
Для передачи больших объёмов данных или прогресса используйте port или серию событий:
// main/ipc-handlers.js — стриминг через messageChannel
ipcMain.handle('fs:readLargeFile', async (event, filePath) => {
const { port1, port2 } = new MessageChannelMain();
// Отправляем port renderer-у
event.sender.postMessage('port', null, [port1]);
// Стримим файл через port
const stream = require('fs').createReadStream(filePath, { encoding: 'utf8' });
stream.on('data', (chunk) => {
port2.postMessage({ type: 'chunk', data: chunk });
});
stream.on('end', () => {
port2.postMessage({ type: 'end' });
port2.close();
});
stream.on('error', (err) => {
port2.postMessage({ type: 'error', message: err.message });
port2.close();
});
});
// renderer — получение порта и чтение стрима
ipcRenderer.on('port', (event) => {
const [port] = event.ports;
let fullContent = '';
port.onmessage = (event) => {
if (event.data.type === 'chunk') {
fullContent += event.data.data;
onProgress?.(fullContent.length);
} else if (event.data.type === 'end') {
onComplete(fullContent);
} else if (event.data.type === 'error') {
onError(event.data.message);
}
};
port.start();
});
await ipcRenderer.invoke('fs:readLargeFile', path);
Broadcast из main во все окна
Когда нужно уведомить все открытые окна:
// main/broadcast.js
const { BrowserWindow } = require('electron');
function broadcast(channel, data) {
BrowserWindow.getAllWindows().forEach(win => {
if (!win.isDestroyed()) {
win.webContents.send(channel, data);
}
});
}
module.exports = { broadcast };
// Использование в любом месте main process
const { broadcast } = require('./broadcast');
broadcast('network:change', { isOnline: true });
broadcast('settings:updated', newSettings);
Отладка IPC
В renderer DevTools (консоль):
// Временное логирование всех IPC событий
const origInvoke = ipcRenderer.invoke.bind(ipcRenderer);
ipcRenderer.invoke = (channel, ...args) => {
console.log('[IPC→]', channel, args);
return origInvoke(channel, ...args).then(result => {
console.log('[IPC←]', channel, result);
return result;
});
};
Типичные ошибки: забыть return true в ipcMain.on при асинхронном ответе через sendResponse; передавать не сериализуемые объекты через IPC (Error объекты, функции, DOM nodes); не обрабатывать ошибки в ipcMain.handle — необработанный Promise rejection крашит обработчик и renderer получит IpcError без деталей.







