Salta al contenuto
Electron

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:

  1. Renderer → Main (unidirezionale)
  2. Renderer → Main → Renderer (bidirezionale con risposta)
  3. 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

  1. Preferisci invoke/handle a send/on — gestisce automaticamente le risposte e gli errori
  2. Valida sempre i dati ricevuti dal renderer nel Main Process
  3. Non esporre canali generici — crea handler specifici per ogni operazione
  4. Centralizza i nomi dei canali in costanti condivise
  5. Gestisci il cleanup dei listener per evitare memory leak
  6. Non inviare dati sensibili tramite IPC senza necessità

Contenuto redatto con l'assistenza di strumenti di intelligenza artificiale e rivisto dalla redazione di Codegrind, che ne è responsabile. Hai trovato un errore? Scrivici.