00
:
00
:
00
:
00
Corso SEO AI - Usa SEOEMAIL al checkout per il 30% di sconto

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à