> Manuales > Manual de Electron

Cómo usar las funcionalidades del IPC (Inter Process Communication) para solicitar la ejecución de funcionalidades que dependan de NodeJS desde el Javascript de la página web que se ejecuta en la ventana de la aplicación.

Solicitar a Electron ejecutar funcionalidad de Node desde la página web

En el artículo anterior vimos cómo Electron es capaz de enviar datos al proceso del lado del frontend, para que luego los podamos presentar al usuario en la página web cargada en la ventana de la aplicación. Para conseguir esta funcionalidad usamos un objeto llamado contextBridge que nos ofrece Electron.

En este artículo queremos ir un poco más allá, realizando un ejemplo sobre cómo podemos ejecutar código NodeJS que realice acciones de bajo nivel sobre el sistema de archivos del ordenador del usuario. Para ello necestaremos aprender nuevas utilidades disponibles en Electron.

Dónde puedo usar la funcionalidad completa de NodeJS

Esto ya lo explicamos antes en el Manual de Electron.js pero, a modo de recordatorio y ampliando un poco más la información, queremos volver a señalar que existen algunas limitaciones en lo que respecta a la funcionalidad disponible desde NodeJS. Todo ello a pesar de estar desarrollando aplicaciones de escritorio y debido a que estamos usando como motor de la aplicación el propio browser.

Por ejemplo, en el código del frontend, que se ejecuta en el lado del cliente, dentro de la página web, seguimos con las limitaciones típicas del desarrollo frontend. Podemos manipular el DOM y acceder a el API del navegador, pero seguimos sin poder hacer cosas como acceder libremente al sistema de archivos del ordenador.

Sin embargo, dentro de nuestra aplicación Electron algunos de los archivos no se ejecutan sobre el navegador, sino que se ejecutan en la plataforma NodeJS, por lo que podré hacer en ellos todo tipo de acciones, como acceder al sistema de ficheros, acceder a bases de datos, programas de consola, etc.

Dentro de los archivos que hemos realizado basados en Node, tenemos dos hasta el momento:

Dado que el renderer process tiene la funcionalidad de Node limitada, no podríamos hacer todo tipo de acciones. Por ejemplo, no podré acceder al sistema de archivos o a packages de npm. Esas acciones las vamos a tener que realizar desde el proceso principal de la aplicación, es decir, desde el archivo main.js que arranca todo.

Usando el IPC

Dado que el usuario interacciona siempre en el contexto de la página web, donde podemos escuchar eventos sobre elementos como botones u otros campos de formulario, cada vez que queramos hacer cosas en el ordenador del usuario que involucren las funcionalidades de NodeJS, necesitaremos de alguna manera avisar al proceso principal para que se encargue de ello. Para ello vamos a usar una de las piezas de la arquitectura del navegador Chromium de las que ya hemos hablado: IPC.

El IPC (inter-process communication) es una pasarela desde la que podemos enviar mensajes desde las capas de la aplicación, básicamente entre el código del frontend que se ejecuta en el renderer process y el código de la aplicación, que se ejecuta en el main process.

Es fundamental el IPC ya que desde el main process no puedo acceder al DOM de la página y desde el código de frontend no puedo acceder a la funcionalidad de NodeJS.

Este componente del navegador se puede usar con funciones que están disponibles en Electron y que vamos a ver a continuación.

Método ipcRenderer.invoke()

Este método nos permite invocar funcionalidad que está dentro del Main process. Es un método del API de Electron, por lo que podemos usarlo desde el archivo preload.js, que se ejecuta en el renderer process.

El método invoke debe de recibir el nombre de la funcionalidad que se desea invocar.

ipcRenderer.invoke('readFile')

En el caso anterior estaríamos llamando a la funcionalidad 'readFile' que podremos implementar en el main process.

Este sería el código de un archivo preload.js que se encarga de exponer una funcionalidad para que se pueda acceder desde el frontend.

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

contextBridge.exposeInMainWorld('doReadFile', () => ipcRenderer.invoke('readFile'));

Recuerda que en el artículo anterior te hemos explicado cómo asociar ese archivo de preload a la página web y te hemos dado otros detalles sobre el objeto contextBridge.

Método ipcMain.handle()

Este método lo podemos usar desde el main process y sirve para manejar solicitudes invocadas desde el renderer process.

Para poder usarlo necesitamos enviarle dos argumentos:

ipcMain.handle('readFile', readLocalFile);

En este caso estamos indicando que se manejará la funcionalidad de nombre 'readFile' y que para procesarla se ejecutará la función readLocalFile().

Esa función readLocalFile() se tendrá que declarar en el main process, codificado en el archivo archivo main.js. Como pertenece al proceso principal, podrá realizar cualquier operativa dependiente de NodeJS. Por ejemplo, en esa función podré acceder al módulo fs para el acceso al file system.

Ahora vamos a ver el código de un archivo main.js completo donde se ha implementado esa mejora para manejar un proceso que nos llega invocado desde el IPC.

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

const createWindow = () => {
  const win = new BrowserWindow({
    width: 400,
    height: 300,
    webPreferences: {
      preload: path.join(__dirname, 'modules', 'preload.js'),
    },
  })
  win.loadFile('index.html');
}

ipcMain.handle('readFile', readLocalFile);

function readLocalFile() {
  let file = path.join(__dirname, 'src', 'files', 'fichero.txt');
  fs.readFile(file, 'utf-8', function (err, data) {
    if (!err) {
      console.log(data);
    } else {
      console.log(err);
    }
  });
}

La mayoría del código de esta aplicación está explicado en artículos anteriores del Manual de Electron. Básicamente el código nuevo es el que permite definir cómo vamos a manejar el proceso "readFile" cuando se solicite vía IPC. Para ello llamaremos a la función readLocalFile() que tenemos declarada al final del código, que se encrgará de acceder a un archivo de texto y mostrar su contenido en la consola de Node.

En el Manual de Node https://desarrolloweb.com/manuales/manual-nodejs.html se ven varias de las funciones de NodeJS que hemos usado aquí, por ejemplo para trabajar con rutas y el acceso a sistema de archivos.

Cómo invocar la funcionalidad desde el código frontend

Solo nos queda ver cómo hemos conseguido invocar la funcionalidad desde el código frontend, pero es algo muy sencillo, ya que hemos expuesto una función mediante el método contextBridge.exposeInMainWorld() del preload script que hemos explicado antes.

El index.html de nuestro proyecto sería como este:

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Invocar funcionalidad vía IPC con Electron</title>
</head>
<body>

  <button id="readbutton">Leer archivo</button>

  <script>
    document.getElementById('readbutton').addEventListener('click', function() {
      doReadFile();
    });
  </script>
</body>
</html>

Como puedes ver, tenemos un botón al cual le hemos asociado un manejador para el evento click. Una vez se hace clic en el botón se invoca la función doReadFile() que nos han enviado mediante contextBridge.exposeInMainWorld().

La invocación de procesos IPC devuelve promesas

En el ejemplo anterior no hemos devuelto nada. Probablemente te preguntarás cómo hacer que ese contenido del archivo de texto llegue al cliente, o cómo el cliente puede conseguir que desde el cliente enviemos una cadena para actualizar el archivo de texto. Eso lo veremos en el próximo artículo. Sin embargo ahora queremos señalar un punto interesante sobre el funcionamiento asíncrono del IPC.

Es importante notar que los procesos del Inter-process Communication devuelven promesas al ejecutarse, incluso aunque lo que se haya solicitado al main process no sea algo asíncrono como en el caso de un simple cálculo matemático. Por tanto, si un proceso envía algo al renderer process, tendremos que esperar a que la promesa se resuelva.

La lectura de un fichero de texto suele ser un proceso asíncrono, aunque también se puede desarrollar con funciones síncronas en Node. De todos modos, sea o no asíncrono, siempre tendremos que esperar que se resuelvan las promesas.

Vamos a ver con un ejemplo de este proceso. Supongamos que necesitamos tener en el main process una función que devuelva algo. En este caso será una cadena aleatoria lo que queremos devolver, aunque podría ser cualquier cosa más compleja que dependiese de una librería y necesitásemos ejecutar en el main process.

En main.js tendríamos este código:

ipcMain.handle('randomString', createRandomString);

function createRandomString() {
  return (Math.random() + 1).toString(36).substring(2);
}

Luego, en el archivo preload.js tendriamos que hacer el contextBridge.exposeInMainWorld() para exponer esa función al frontend.

contextBridge.exposeInMainWorld('doRandomString', () => ipcRenderer.invoke('randomString'));

Ahora, en el Javascript del lado del cliente deberíamos esperar la resolución de la promesa para poder usar el dato devuelto por el main process.

doRandomString().then(
  randomString => alert(randomString)
);

Conclusión

Con esto ya hemos terminado de ilustrar el mecanismo para solicitar al main process la realización de acciones que dependen de la funcionalidad completa de NodeJS. A partir de aquí tienes todo el poder de Node a tu alcance también desde el código del frontend.

Así es como vamos a organizarnos para poder trabajar cuando desarrollemos aplicaciones bajo Electron. Parece algo raro y demasiado aparatoso, pero realmente la necesidad de realizar este proceso complejo nos viene por las restricciones impuestas por la arquitectura del propio Chromium. Poco a poco te parecerá más natural.

Miguel Angel Alvarez

Fundador de DesarrolloWeb.com y la plataforma de formación online EscuelaIT. Com...

Manual