Estudiamos los procesos de comunicación de las capas de aplicación de Electron, entre renderer process y main process mediante IPC. Implementamos los mecanismos de comunicación para crear un rudimentario editor de textos.
En el pasado artículo comenzamos a practicar con los mecanismos de IPC (Inter-process Communication) que nos sirve para permitir que los procesos del main process y el renderer process puedan conversar.
De modo teórico ya explicamos también el esquema de procesos de Electron y qué función desempeña el IPC.
En este artículo vamos a continuar con la práctica ya comenzada, para explicar cómo podremos enviar y recibir datos desde el renderer process hacia el main process de Electron. En esta práctica realizaremos dos procesos que nos permitan implementar un rudimentario editor de texto, donde se podrá leer y escribir en un archivo de texto del ordenador del usuario.
En el pasado artículo realizamos una invocación de un proceso en el main process que abría el archivo de texto y lo mostraba en el terminal de línea de comandos desde el main process, pero lo que seguramente querrás hacer es enviar el contenido de ese archivo al frontend para que se visualice en la aplicación. Además queremos hacer una invocación a otro proceso que nos permita guardar un fichero de texto en el ordenador del usuario. Para ello enviaremos el contenido del archivo de texto desde el frontend.
Por supuesto, al tratarse de una lectura y escritura en una carpeta del disco duro del usuario, es necesario realizarla desde el main process, que es quien tiene la posibilidad de usar los mecanismos de Node sin limitaciones.
Los pasos para realizar esta práctica incluye crear un par de canales de comunicación en el IPC, en el que enviar mensajes cuando queramos leer o escribir en el archivo de texto. Como aprendimos en el artículo anterior, desde el renderer process realizamos ipcRenderer.invoke()
para invocar el mensaje en el canal indicado como argumento y desde el main process realizamos ipcMain.handle()
para ejecutar acciones cuando nos llegue el mensaje al main process. Como las bases de este proceso ya las conoces, pues lo hemos visto anteriormente, vamos a comenzar explicando el código del frontend que es el que comienza el flujo de comunicación.
Código en el frontend
En el frontend tenemos un campo textarea que contiene el texto que queremos guardar en el archivo y un par de botones, el primero para recuperar el contenido del archivo de texto y el segundo botón para que se almacene cuando se haga clic.
Todo este código se encontrará en el archivo index.html
que se muestra en la ventana al arrancar la aplicación.
<form>
<label for="file">Texto:</label>
<textarea name="file" id="file" cols="30" rows="7"></textarea>
<br>
<button id="readbutton">Leer archivo</button>
<button id="savebutton">Guardar archivo</button>
</form>
Todo ese marcado lo tenemos dentro de un formulario, aunque nos podríamos haber ahorrado la etiqueta
<form>
ya que en realidad no vamos a hacer nada con ella.
Los manejadores de eventos de ambos botones los podemos ver ahora, comenzando por el botón de lectura del archivo.
document.getElementById('readbutton').addEventListener('click', function (e) {
e.preventDefault();
restoreFile();
});
Este primer botón, cuando se produzca el clic, invocará a una función que se llama restoreFile()
y que podemos ver a continuación.
function restoreFile() {
doReadFile().then(text => document.getElementById('file').value = text);
}
Esta función es la que realiza el proceso de obtención del contenido del archivo de texto. Ese archivo nos llegará desde el main process y para poder comunicar con ese proceso principal habrá una función llamada doReadFile()
que se habrá enviado desde el archivo preload.js
que se ejecuta en el renderer process.
Tenemos que considerar que doReadFile()
es una función asíncrona, dado que el acceso al sistema de archivos es asíncrono. Por ello lo que devolverá esa función es una promesa. Cuando se resuelva la promesa se recibirá el texto del archivo. y con él se actualizará el contenido del textarea.
Además, nada más arrancar la página se llama también a restoreFile()
para que se muestre el contenido del archivo de texto en el textarea.
restoreFile();
Ahora vamos a ver el un manejador de evento click
para el segundo botón, que invoca la función que nos permite guardar el contenido en el archivo de texto.
document.getElementById('savebutton').addEventListener('click', function (e) {
e.preventDefault();
let text = document.getElementById('file').value;
doSaveFile(text).then(() => console.log('salvado!!') );
});
En el manejador de eventos simplemente accedemos al contenido del textarea y luego invocamos una función llamada doSaveFile()
a la que le enviamos el texto que queremos salvar. La función doSaveFile()
también devuelve una promesa que se resolverá cuando se haya terminado de guardar el archivo de texto, solo que en este caso no nos devuelve nada en concreto y simplemente mostramos un mensaje en la consola del navegador que dice "salvado!!".
Lo suyo sería mostrar ese mensaje al usuario de alguna manera en la interfaz de la aplicación, pero eso ya lo dejamos para ti..
Script de preload
En el script de preload, que se ejecuta en NodeJS pero dentro del contexto del renderer process, para la página actual, se encuentran las funciones en las que nos hemos apoyado en el código de la página web:
Función doReadFile()
Se encarga de leer el archivo y devolver una promesa que se resolverá con el contenido del archivo leído. Ya la vimos en el anterior artículo.
contextBridge.exposeInMainWorld('doReadFile', () => ipcRenderer.invoke('readFile'));
Como sabes, usamos
contextBridge.exposeInMainWorld()
para exponer datos o funciones que podemos acceder luego desde el código de la página web.
En este archivo no se muestra el código de la función, ya que se encuentra en el main process. Lo único que hacemos es establecer el puente, vía IPC, de esa función disponible en el renderer process con el main process.
Función doSaveFile(text)
Esta segunda función nos permite hacer la escritura del fichero. La novedad aquí es que necesita recibir el contenido del archivo que debe escribirse.
contextBridge.exposeInMainWorld('doSaveFile', (text) => ipcRenderer.invoke('saveFile', text));
Además, para enviar esos datos vía IPC al main process usaremos un segundo parámetro del método ipcRenderer.invoke()
. El primer parámetro ya lo habíamos usado, que es el nombre del canal en el IPC que vamos a utilizar y el segundo parámetro es el dato que le queremos enviar al main process.
Main process
Ahora ya solamente nos queda definir los comportamientos de lectura y escritura en el archivo de texto. Estos comportamientos los hacemos en el main process, que es quien tiene la posibilidad de ejecutar cualquier funcionalidad de NodeJS.
Primero tenemos que usar los métodos ipcMain.handle()
para declarar los canales de comunicación que vamos a estar escuchando.
ipcMain.handle('readFile', readLocalFile);
ipcMain.handle('saveFile', saveLocalFile);
Cuando esos canales reciban mensajes se invocarán las funciones readLocalFile()
y saveLocalFile()
. La gracia de estos dos procesos es que devolverán promesas que tenemos que generar en nuestro propio código.
Por si alguna vez no has hecho métodos que devuelven promesas te dejamos el enlace al artículo de Implementar Promesas en Javascript donde podrás aprender.
El código de la función que lee el archivo es el siguiente.
let fs = require('fs');
const path = require('path');
const filePath = path.resolve('src', 'files', 'fichero.txt');
function readLocalFile() {
return new Promise( (resolve, reject) => {
fs.readFile(filePath, 'utf-8', function (err, data) {
if (err) {
reject(err);
} else {
resolve(data);
}
});
});
}
- Comenzamos por requerir los módulos de Node que necesitamos para esta tarea.
- Luego hemos creado una constante que se encarga de declarar la ruta del fichero. Lo hemos hecho fuera de la función porque esa ruta la usaremos tanto para la lectura como para la escritura.
- Luego tenemos la función
readLocalFile()
que es donde se hace el proceso de lectura del archivo y se devuelve una promesa que se resuelve positiva o negativamente.
Ahora veamos la función que escribe el contenido del archivo de texto, que hemos llamado saveLocalFile()
y que también devuelve una promesa que se resolverá cuando se consiga, o no, hacer esa escritura.
function saveLocalFile(event, text) {
return new Promise((resolve, reject) => {
fs.writeFile(filePath, text, (err) => {
if(err) {
reject(err);
} else {
resolve(true);
}
});
});
}
Esto es todo lo que hemos necesitado hacer para implementar nuestro rudimentario editor de textos. No tiene demasiada complejidad si entendemos la manera de trabajar con Electron y el esquema de procesos (principal y renderer), así como la necesidad de usar el IPC para comunicar entre ellos.
Miguel Angel Alvarez
Fundador de DesarrolloWeb.com y la plataforma de formación online EscuelaIT. Com...