La communication IPC d'Electron expliquée avec des exemples réels

Diagramme de communication inter-processus d'Electron

Electron est l’un des frameworks les plus populaires pour créer des applications de bureau multi-plateformes en utilisant des technologies web telles que l’HTML, le CSS et le JavaScript. Sous le capot, Electron utilise une architecture multi-processus composée d’un Processus Principal (qui exécute Node.js) et d’un ou plusieurs Processus de Rendu (qui exécutent Chromium pour afficher l’interface utilisateur).

Pour des raisons de sécurité, les applications Electron modernes isolent le Processus de Rendu du système d’exploitation. Cela signifie que vous ne pouvez pas accéder directement aux modules Node.js ou aux ressources système (comme lire des fichiers ou interroger des bases de données) depuis l’interface utilisateur du Renderer.

Pour combler cet écart en toute sécurité, Electron utilise la Communication Entre Processus (IPC).

Dans ce guide, nous expliquerons comment fonctionne l’IPC d’Electron et explorerons les trois modèles de communication fondamentaux avec des exemples de code réels et prêts pour la production.


1. Du Renderer au Main (Unidirectionnel / One-Way)

Ce modèle est utilisé lorsque le Renderer souhaite envoyer une commande ou une action au processus Principal sans attendre de réponse. Un exemple courant consiste à cliquer sur un bouton de l’interface utilisateur pour réduire ou fermer la fenêtre de l’application.

Voyons comment cela est implémenté dans les trois fichiers clés : main.js, preload.js et renderer.js.

Processus Principal (main.js)

Nous utilisons ipcMain.on pour écouter les événements du processus de rendu.

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');
}

// Écouter l'événement 'close-app' du renderer
ipcMain.on('close-app', () => {
  app.quit();
});

Script de Préchargement (preload.js)

Nous utilisons contextBridge.exposeInMainWorld pour exposer un wrapper sécurisé au renderer sans exposer l’intégralité du module ipcRenderer.

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

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

Processus de Rendu (renderer.js)

Nous appelons la fonction exposée sur l’objet window.

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

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

2. Du Renderer au Main (Bidirectionnel / Requête-Réponse)

Ce modèle est utilisé lorsque le Renderer doit demander des données ou déclencher une opération système à partir du processus Principal et attendre le résultat (par exemple, lire un fichier ou effectuer une requête de base de données sécurisée).

Nous utilisons ipcMain.handle dans le processus Principal et ipcRenderer.invoke dans le script de Préchargement.

Processus Principal (main.js)

Écouter la requête en utilisant ipcMain.handle et renvoyer les données de manière asynchrone.

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

// Gérer l'invocation 'read-file' de manière asynchrone
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 de Préchargement (preload.js)

Exposer un wrapper asynchrone qui renvoie une Promesse (Promise).

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

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

Processus de Rendu (renderer.js)

Utiliser await pour attendre que la Promesse renvoyée soit résolue.

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('Contenu du fichier :', result.content);
  } else {
    console.error('Échec de lecture du fichier :', result.error);
  }
});

3. Du Main au Renderer (Notification Unidirectionnelle)

Ce modèle est utilisé lorsque le processus Principal doit envoyer des mises à jour ou des notifications au processus de Rendu (par exemple, des mises à jour de barre de progression de téléchargement, des clics sur les menus de l’application ou des mises à jour d’état de service en arrière-plan).

Nous utilisons webContents.send dans le processus Principal et ipcRenderer.on dans le script de Préchargement.

Processus Principal (main.js)

Obtenir le webContents de la fenêtre active et envoyer le message.

// Exemple : Envoi de mises à jour de progression du téléchargement
function trackDownloadProgress(mainWindow) {
  let progress = 0;
  const interval = setInterval(() => {
    progress += 10;
    mainWindow.webContents.send('download-progress', progress);
    
    if (progress >= 100) {
      clearInterval(interval);
    }
  }, 1000);
}

Script de Préchargement (preload.js)

Exposer une méthode d’abonnement qui prend une fonction de callback. Il est recommandé de renvoyer une fonction de nettoyage pour supprimer le listener.

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

contextBridge.exposeInMainWorld('electronAPI', {
  onDownloadProgress: (callback) => {
    const subscription = (event, value) => callback(value);
    ipcRenderer.on('download-progress', subscription);
    
    // Renvoyer la fonction de nettoyage pour éviter les fuites de mémoire
    return () => {
      ipcRenderer.removeListener('download-progress', subscription);
    };
  }
});

Processus de Rendu (renderer.js)

S’abonner aux événements et mettre à jour l’interface utilisateur.

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

const unsubscribe = window.electronAPI.onDownloadProgress((progress) => {
  progressBar.style.width = `${progress}%`;
  progressBar.textContent = `${progress}%`;
  
  if (progress === 100) {
    console.log('Téléchargement terminé !');
    unsubscribe(); // Nettoyer le listener pour éviter les fuites de mémoire
  }
});

4. Bonnes Pratiques de Sécurité

Lorsque vous travaillez avec l’IPC d’Electron, la sécurité doit être votre priorité absolue. Un code malveillant injecté dans le processus de Rendu peut compromettre l’ensemble du système d’exploitation si l’IPC n’est pas sécurisé.

Suivez ces règles cruciales :

  1. Ne jamais exposer ipcRenderer directement : N’écrivez pas contextBridge.exposeInMainWorld('electron', ipcRenderer). Cela donne au renderer un accès complet pour envoyer n’importe quel message IPC, contournant ainsi les barrières de sécurité.
  2. Garder l’isolation du contexte activée : Définissez toujours contextIsolation: true et nodeIntegration: false dans webPreferences.
  3. Valider les entrées : Validez toujours les arguments reçus dans le processus Principal (comme les chemins de fichiers ou les requêtes de base de données) avant de les exécuter.
  4. Préférer ipcMain.handle à ipcMain.on + webContents.send : Pour les modèles de requête-réponse, invoke/handle is cleaner, résout les Promesses nativement et évite de mélanger plusieurs callbacks d’écoute.

Explorez d’autres perspectives de développement sur le blog de Ghaznix →