IPC Communication
IPC (Inter-Process Communication) è il meccanismo che permette ai processi Main e Renderer di scambiarsi messaggi. Poiché i due processi sono isolati, IPC è l’unico modo per comunicare tra di essi.
Pattern di Comunicazione
Electron offre tre pattern principali di IPC:
- Renderer → Main (unidirezionale)
- Renderer → Main → Renderer (bidirezionale con risposta)
- Main → Renderer (unidirezionale)
Pattern 1: Renderer → Main (Fire and Forget)
Usa ipcRenderer.send() e ipcMain.on() quando non hai bisogno di una risposta:
// preload.js
const { contextBridge, ipcRenderer } = require('electron');
contextBridge.exposeInMainWorld('electronAPI', {
setTitle: (title) => ipcRenderer.send('set-title', title),
minimizeWindow: () => ipcRenderer.send('window:minimize'),
});
// main.js
const { ipcMain, BrowserWindow } = require('electron');
ipcMain.on('set-title', (event, title) => {
const win = BrowserWindow.fromWebContents(event.sender);
win.setTitle(title);
});
ipcMain.on('window:minimize', (event) => {
BrowserWindow.fromWebContents(event.sender).minimize();
});
// renderer.js
document.getElementById('title-input').addEventListener('change', (e) => {
window.electronAPI.setTitle(e.target.value);
});
Pattern 2: Renderer → Main con Risposta (Invoke/Handle)
Usa ipcRenderer.invoke() e ipcMain.handle() quando serve una risposta. Questo è il pattern più consigliato:
// preload.js
contextBridge.exposeInMainWorld('electronAPI', {
openFile: () => ipcRenderer.invoke('dialog:openFile'),
readFile: (path) => ipcRenderer.invoke('fs:readFile', path),
getSystemInfo: () => ipcRenderer.invoke('system:getInfo'),
});
// main.js
const { ipcMain, dialog } = require('electron');
const fs = require('node:fs/promises');
const os = require('node:os');
ipcMain.handle('dialog:openFile', async () => {
const { canceled, filePaths } = await dialog.showOpenDialog({
properties: ['openFile'],
});
if (canceled) return null;
return filePaths[0];
});
ipcMain.handle('fs:readFile', async (_event, filePath) => {
const content = await fs.readFile(filePath, 'utf-8');
return content;
});
ipcMain.handle('system:getInfo', () => {
return {
platform: process.platform,
arch: process.arch,
cpus: os.cpus().length,
memory: Math.round(os.totalmem() / 1024 / 1024 / 1024) + ' GB',
hostname: os.hostname(),
};
});
// renderer.js
document.getElementById('open-btn').addEventListener('click', async () => {
const filePath = await window.electronAPI.openFile();
if (filePath) {
const content = await window.electronAPI.readFile(filePath);
document.getElementById('editor').value = content;
}
});
async function showSystemInfo() {
const info = await window.electronAPI.getSystemInfo();
console.log(info); // { platform: 'win32', arch: 'x64', ... }
}
Gestione degli Errori con Invoke/Handle
Gli errori lanciati nel handle vengono propagati automaticamente al invoke:
// main.js
ipcMain.handle('fs:readFile', async (_event, filePath) => {
try {
return await fs.readFile(filePath, 'utf-8');
} catch (error) {
throw new Error(`Impossibile leggere il file: ${error.message}`);
}
});
// renderer.js
try {
const content = await window.electronAPI.readFile('/path/inesistente');
} catch (error) {
console.error('Errore:', error.message);
// "Impossibile leggere il file: ENOENT..."
}
Pattern 3: Main → Renderer
Usa webContents.send() dal Main Process per inviare messaggi al Renderer:
// main.js
const { Menu, BrowserWindow } = require('electron');
function createMenu(mainWindow) {
const template = [
{
label: 'File',
submenu: [
{
label: 'Nuovo',
accelerator: 'CmdOrCtrl+N',
click: () => {
// Invia messaggio al renderer
mainWindow.webContents.send('menu:action', 'new-file');
},
},
{
label: 'Apri',
accelerator: 'CmdOrCtrl+O',
click: () => {
mainWindow.webContents.send('menu:action', 'open-file');
},
},
],
},
];
Menu.setApplicationMenu(Menu.buildFromTemplate(template));
}
// preload.js
contextBridge.exposeInMainWorld('electronAPI', {
onMenuAction: (callback) => {
ipcRenderer.on('menu:action', (_event, action) => callback(action));
},
});
// renderer.js
window.electronAPI.onMenuAction((action) => {
switch (action) {
case 'new-file':
editor.value = '';
break;
case 'open-file':
openFileDialog();
break;
}
});
Comunicazione tra Renderer Process
Due Renderer Process non possono comunicare direttamente. Il Main Process funge da intermediario:
// main.js - Il Main Process fa da ponte
let mainWindow, settingsWindow;
ipcMain.on('settings:changed', (_event, settings) => {
// Ricevi dal settings window, inoltra al main window
if (mainWindow && !mainWindow.isDestroyed()) {
mainWindow.webContents.send('settings:updated', settings);
}
});
Canali con Tipizzazione
Per evitare errori di digitazione nei nomi dei canali, centralizzali in un file condiviso:
// shared/channels.js
module.exports = {
FILE: {
OPEN: 'file:open',
SAVE: 'file:save',
READ: 'file:read',
RECENT: 'file:recent',
},
WINDOW: {
MINIMIZE: 'window:minimize',
MAXIMIZE: 'window:maximize',
CLOSE: 'window:close',
},
MENU: {
ACTION: 'menu:action',
},
THEME: {
GET: 'theme:get',
SET: 'theme:set',
CHANGED: 'theme:changed',
},
};
// preload.js
const { FILE, WINDOW } = require('../shared/channels');
contextBridge.exposeInMainWorld('electronAPI', {
openFile: () => ipcRenderer.invoke(FILE.OPEN),
saveFile: (data) => ipcRenderer.invoke(FILE.SAVE, data),
minimizeWindow: () => ipcRenderer.send(WINDOW.MINIMIZE),
});
MessagePort (Comunicazione Avanzata)
Per scenari avanzati dove serve comunicazione diretta ad alte prestazioni tra due Renderer, Electron supporta i MessagePort:
// main.js
const { MessageChannelMain } = require('electron');
app.whenReady().then(() => {
const window1 = new BrowserWindow({ webPreferences: { preload: '...' } });
const window2 = new BrowserWindow({ webPreferences: { preload: '...' } });
const { port1, port2 } = new MessageChannelMain();
// Invia un port a ciascuna finestra
window1.webContents.postMessage('port', null, [port1]);
window2.webContents.postMessage('port', null, [port2]);
});
// preload.js (per entrambe le finestre)
const { ipcRenderer } = require('electron');
ipcRenderer.on('port', (event) => {
const port = event.ports[0];
contextBridge.exposeInMainWorld('directChannel', {
send: (data) => port.postMessage(data),
onMessage: (callback) => {
port.onmessage = (event) => callback(event.data);
},
});
});
Best Practice
- Preferisci
invoke/handleasend/on— gestisce automaticamente le risposte e gli errori - Valida sempre i dati ricevuti dal renderer nel Main Process
- Non esporre canali generici — crea handler specifici per ogni operazione
- Centralizza i nomi dei canali in costanti condivise
- Gestisci il cleanup dei listener per evitare memory leak
- Non inviare dati sensibili tramite IPC senza necessità