La comunicación IPC de Electron explicada con ejemplos reales

Diagrama de comunicación interprocesos de Electron

Electron es uno de los frameworks más populares para crear aplicaciones de escritorio multiplataforma utilizando tecnologías web como HTML, CSS y JavaScript. Internamente, Electron ejecuta una arquitectura multiproceso que consta de un Proceso Principal (que ejecuta Node.js) y uno o más Procesos de Renderizado (que ejecutan Chromium para renderizar la interfaz de usuario).

Debido a los riesgos de seguridad, las aplicaciones Electron modernas aíslan el Proceso de Renderizado del sistema operativo. Esto significa que no puede acceder a los módulos de Node.js ni a los recursos del sistema (como leer archivos o realizar consultas en bases de datos) directamente desde la interfaz de usuario del Renderizador.

Para salvar esta brecha de forma segura, Electron utiliza la Comunicación Interprocesos (IPC).

En esta guía, explicaremos cómo funciona el IPC de Electron y exploraremos los tres patrones de comunicación fundamentales con ejemplos de código reales y listos para producción.


1. Del Renderizador al Principal (Unidireccional / One-Way)

Este patrón se utiliza cuando el Renderizador desea enviar un comando o acción al proceso Principal sin esperar ninguna respuesta. Un ejemplo común es hacer clic en un botón de la interfaz de usuario para minimizar o cerrar la ventana de la aplicación.

Veamos cómo se implementa en los tres archivos clave: main.js, preload.js y renderer.js.

Proceso Principal (main.js)

Usamos ipcMain.on para escuchar eventos del proceso de renderizado.

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

// Escuchar el evento 'close-app' desde el renderizador
ipcMain.on('close-app', () => {
  app.quit();
});

Script de Precarga (preload.js)

Usamos contextBridge.exposeInMainWorld para exponer un wrapper seguro al renderizador sin exponer todo el módulo ipcRenderer.

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

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

Proceso del Renderizador (renderer.js)

Llamamos a la función expuesta en el objeto window.

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

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

2. Del Renderizador al Principal (Bidireccional / Solicitud-Respuesta)

Este patrón se utiliza cuando el Renderizador necesita solicitar datos o activar una operación del sistema desde el proceso Principal y esperar el resultado (por ejemplo, leer un archivo o realizar una consulta segura a una base de datos).

Usamos ipcMain.handle en el proceso Principal e ipcRenderer.invoke en el script de Precarga.

Proceso Principal (main.js)

Escucha la solicitud usando ipcMain.handle y devuelve los datos de forma asíncrona.

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

// Manejar la invocación 'read-file' de forma asíncrona
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 Precarga (preload.js)

Expone un wrapper asíncrono que devuelve una Promesa (Promise).

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

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

Proceso del Renderizador (renderer.js)

Usa await para esperar a que se resuelva la Promesa devuelta.

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('Contenido del archivo:', result.content);
  } else {
    console.error('Error al leer el archivo:', result.error);
  }
});

3. Del Principal al Renderizador (Notificación Unidireccional)

Este patrón se utiliza cuando el proceso Principal necesita enviar actualizaciones o notificaciones al proceso del Renderizador (por ejemplo, actualizaciones de la barra de progreso de descarga, clics en los menús de la aplicación o actualizaciones del estado del servicio en segundo plano).

Usamos webContents.send en el proceso Principal e ipcRenderer.on en el script de Precarga.

Proceso Principal (main.js)

Obtiene el webContents de la ventana activa y envía el mensaje.

// Ejemplo: Envío de actualizaciones de progreso de descarga
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 Precarga (preload.js)

Expone un método de suscripción que recibe una función de callback. Se recomienda devolver una función de limpieza para eliminar el listener.

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

contextBridge.exposeInMainWorld('electronAPI', {
  onDownloadProgress: (callback) => {
    const subscription = (event, value) => callback(value);
    ipcRenderer.on('download-progress', subscription);
    
    // Devolver función de limpieza para evitar fugas de memoria
    return () => {
      ipcRenderer.removeListener('download-progress', subscription);
    };
  }
});

Proceso del Renderizador (renderer.js)

Suscríbete a los eventos y actualiza la interfaz de usuario.

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

const unsubscribe = window.electronAPI.onDownloadProgress((progress) => {
  progressBar.style.width = `${progress}%`;
  progressBar.textContent = `${progress}%`;
  
  if (progress === 100) {
    console.log('¡Descarga completada!');
    unsubscribe(); // Limpiar el listener para evitar fugas de memoria
  }
});

4. Buenas Prácticas de Seguridad

Al trabajar con el IPC de Electron, la seguridad debe ser su máxima prioridad. El código malicioso inyectado en el proceso de Renderizado puede comprometer todo el sistema operativo si el IPC no está protegido.

Siga estas reglas fundamentales:

  1. Nunca exponga ipcRenderer directamente: No escriba contextBridge.exposeInMainWorld('electron', ipcRenderer). Al hacer esto, le otorga al renderizador acceso completo para enviar cualquier mensaje IPC, eludiendo los límites de seguridad.
  2. Mantenga habilitado el aislamiento de contexto: Configure siempre contextIsolation: true y nodeIntegration: false en webPreferences.
  3. Valide las entradas: Siempre valide los argumentos recibidos en el proceso Principal (como rutas de archivos o consultas a bases de datos) antes de ejecutarlos.
  4. Prefiera ipcMain.handle en lugar de ipcMain.on + webContents.send: Para los patrones de solicitud-respuesta, invoke/handle es más limpio, resuelve Promesas de forma nativa y evita mezclar múltiples callbacks de escucha.

Conclusión

Comprender la comunicación IPC es la clave para crear aplicaciones Electron seguras, eficientes y sólidas. Al utilizar el patrón adecuado para cada tarea y aplicar el aislamiento de contexto, puede aprovechar toda la potencia de Node.js en el escritorio mientras mantiene seguros a sus usuarios.


Explore más perspectivas de desarrollo en el blog de Ghaznix →