La comunicazione Electron IPC spiegata con esempi reali

Diagramma della comunicazione interprocesso di Electron

Electron è uno dei framework più popolari per la creazione di applicazioni desktop multipiattaforma utilizzando tecnologie web come HTML, CSS e JavaScript. Sotto il cofano, Electron esegue un’architettura a processi multipli composta da un Processo Principale (che esegue Node.js) e uno o più Processi Renderer (che eseguono Chromium per il rendering dell’interfaccia utente).

A causa di rischi di segurança, le moderne applicazioni Electron isolano il processo Renderer dal sistema operativo. Ciò significa che non è possibile accedere ai moduli Node.js o alle risorse di sistema (come la lettura di file o l’esecuzione di query sui database) direttamente dall’interfaccia utente del Renderer.

Per colmare questo divario in modo sicuro, Electron utilizza la Comunicazione Interprocesso (IPC).

In questa guida spiegheremo come funziona la comunicazione IPC di Electron ed esploreremo i tre modelli di comunicazione fondamentali con esempi di codice reali e pronti per la produzione.


1. Da Renderer a Principale (Unidirezionale / One-Way)

Questo modello viene utilizzato quando il Renderer desidera inviare un comando o un’azione al processo Principale senza attendere alcuna risposta. Un esempio comune è fare clic su un pulsante nell’interfaccia utente per ridurre a icona o chiudere la finestra dell’applicazione.

Vediamo come viene implementato nei tre file chiave: main.js, preload.js e renderer.js.

Processo Principale (main.js)

Utilizziamo ipcMain.on per ascoltare gli eventi provenienti dal processo renderer.

const { app, BrowserWindow, ipcMain } = require('electron');
const path = require('path');

function createWindow() {
  const win = new BrowserWindow({
    width: 800,
    height: 600,
    webPreferences: {
      preload: path.join(__dirname, 'preload.js'),
      contextIsolation: true,
      nodeIntegration: false
    }
  });
  win.loadFile('index.html');
}

// Ascolta l'evento 'close-app' dal renderer
ipcMain.on('close-app', () => {
  app.quit();
});

Script di Precaricamento (preload.js)

Utilizziamo contextBridge.exposeInMainWorld per esporre un wrapper sicuro al renderer senza esporre l’intero modulo ipcRenderer.

const { contextBridge, ipcRenderer } = require('electron');

contextBridge.exposeInMainWorld('electronAPI', {
  closeApp: () => ipcRenderer.send('close-app')
});

Processo Renderer (renderer.js)

Chiamiamo la funzione esposta sull’oggetto window.

const closeButton = document.getElementById('close-btn');

closeButton.addEventListener('click', () => {
  window.electronAPI.closeApp();
});

2. Da Renderer a Principale (Bidirezionale / Richiesta-Risposta)

Questo modello viene utilizzato quando il Renderer deve richiedere dati o attivare un’operazione di sistema dal processo Principale e attendere il risultato (ad esempio, la lettura di un file o l’esecuzione di una query sicura sul database).

Utilizziamo ipcMain.handle nel processo Principale e ipcRenderer.invoke nello script di Preload.

Processo Principale (main.js)

Ascolta la richiesta utilizzando ipcMain.handle e restituisce i dati in modo asincrono.

const { ipcMain } = require('electron');
const fs = require('fs/promises');

// Gestisce l'invocazione 'read-file' in modo asincrono
ipcMain.handle('read-file', async (event, filePath) => {
  try {
    const data = await fs.readFile(filePath, 'utf-8');
    return { success: true, content: data };
  } catch (error) {
    return { success: false, error: error.message };
  }
});

Script di Precaricamento (preload.js)

Espone un wrapper asincrono che restituisce una Promise.

const { contextBridge, ipcRenderer } = require('electron');

contextBridge.exposeInMainWorld('electronAPI', {
  readFile: (filePath) => ipcRenderer.invoke('read-file', filePath)
});

Processo Renderer (renderer.js)

Usa await per attendere la risoluzione della Promise restituita.

const readBtn = document.getElementById('read-btn');

readBtn.addEventListener('click', async () => {
  const result = await window.electronAPI.readFile('/path/to/file.txt');
  if (result.success) {
    console.log('Contenuto del file:', result.content);
  } else {
    console.error('Impossibile leggere il file:', result.error);
  }
});

3. Da Principale a Renderer (Notifica Unidirezionale)

Questo modello viene utilizzato quando il processo Principale deve inviare aggiornamenti o notifiche al processo Renderer (ad esempio, aggiornamenti della barra di avanzamento del download, clic sui menu dell’applicazione o aggiornamenti dello stato del servizio in background).

Utilizziamo webContents.send nel processo Principale e ipcRenderer.on nello script di Preload.

Processo Principale (main.js)

Ottiene i webContents della finestra attiva e invia il messaggio.

// Esempio: Invio di aggiornamenti sullo stato del download
function trackDownloadProgress(mainWindow) {
  let progress = 0;
  const interval = setInterval(() => {
    progress += 10;
    mainWindow.webContents.send('download-progress', progress);
    
    if (progress >= 100) {
      clearInterval(interval);
    }
  }, 1000);
}

Script di Precaricamento (preload.js)

Espone un metodo di sottoscrizione che accetta una funzione di callback. È consigliabile restituire una funzione di pulizia per rimuovere il listener.

const { contextBridge, ipcRenderer } = require('electron');

contextBridge.exposeInMainWorld('electronAPI', {
  onDownloadProgress: (callback) => {
    const subscription = (event, value) => callback(value);
    ipcRenderer.on('download-progress', subscription);
    
    // Restituisce la função di pulizia per prevenire perdite di memoria
    return () => {
      ipcRenderer.removeListener('download-progress', subscription);
    };
  }
});

Processo Renderer (renderer.js)

Iscriviti agli eventi e aggiorna l’interfaccia utente.

const progressBar = document.getElementById('progress-bar');

const unsubscribe = window.electronAPI.onDownloadProgress((progress) => {
  progressBar.style.width = `${progress}%`;
  progressBar.textContent = `${progress}%`;
  
  if (progress === 100) {
    console.log('Contenuto scaricato completamente!');
    unsubscribe(); // Pulisce il listener per evitare perdite di memoria
  }
});

4. Best Practice di Sicurezza

Quando si lavora con Electron IPC, la sicurezza deve essere la priorità assoluta. Il codice dannoso iniettato nel processo Renderer può compromettere l’intero sistema operativo se l’IPC non è protetto.

Segui queste regole cruciali:

  1. Non esporre mai direttamente ipcRenderer: Non scrivere contextBridge.exposeInMainWorld('electron', ipcRenderer). Ciò consente al renderer di inviare qualsiasi messaggio IPC, aggirando le barriere di sicurezza.
  2. Mantieni abilitato l’isolamento del contesto: Imposta sempre contextIsolation: true e nodeIntegration: false nelle webPreferences.
  3. Valida gli input: Convalida sempre gli argomenti ricevuti nel processo Principale (come i percorsi dei file o le query del database) prima di eseguirli.
  4. Preferisci ipcMain.handle rispetto a ipcMain.on + webContents.send: Per i modelli richiesta-risposta, invoke/handle è più pulito, risolve le Promise in modo nativo ed evita di mescolare più callback di ascolto.

Conclusione

Comprendere la comunicazione IPC è la chiave per creare applicazioni Electron sicure, efficienti e robuste. Utilizzando il modello corretto per ogni attività e applicando l’isolamento del contesto, puoi sfruttare tutta la potenza di Node.js sul desktop garantendo la sicurezza degli utenti.


Esplora altri approfondimenti per sviluppatori sul blog di Ghaznix →