Electron js desde cero
En este artículo hablamos de electron js, un framework javascript para desarrollar aplicaciones de escritorio.
Parece imparable el avance de Javascript hoy en día. Se impuso en el front-end hace más de una década y posteriormente se hizo un hueco en el back-end, e incluso no hace mucho, se ha colado también en las aplicaciones de escritorio (desktop app) de la mano de electron js. En este tutorial vamos a comentar primero algunos conceptos sobre electron.js y posteriormente vamos a crear un ejemplo (no es el Hola mundo), de una aplicación para validar json.
Qué es electron js?
Electron js es una librería open source desarrollada por GitHub para desarrollar aplicaciones de escritorio con Javascript. Para la parte visual utiliza HTML y CSS. Para ello utiliza librerías de renderizado de Chromium y librerías de Node.js. Históricamente se originó de un framework que se utilizó para crear Atom, un editor de texto de GitHub. Actualmente es posible crear aplicaciones multiplataforma, tanto para Windows, como para Mac OS y Linux.
Cómo funciona electron js?
A grandes rasgos, consta de dos procesos principales, el proceso principal (main process) y el proceso de renderizado (renderer process). El main process es el proceso que ejecuta el script principal especificado en el fichero package.json. Este proceso puede mostrar el interfaz gráfico a partir de páginas web. Esto se realiza creando instancias de BrowserWindow. Cada instancia ejecuta la página web en su propio proceso. Así pues el main process se encarga de gestionar todas estas páginas web a partir de sus respectivos renderer process. Para hacer llamadas a la GUI nativa del sistema, no se permite hacerlo desde el renderer process, sino que debe realizarse desde el main process, para ello hay disponible comunicación entre ambos procesos. Estos procesos para comunicarse son ipcRenderer y ipcMain para enviar mensajes y el módulo remoto para comunicación estilo RPC. El renderer process se apoya en librerias de Chromium para mostrar páginas web. Cada página web se ejecuta en su propio proceso.
En qué plataformas funciona?
Actualmente electron js es compatible con:
- MacOS de 64bit, a partir de la versión 10.9.
- Windows 7 y posteriores, tanto las versiónes de 32bit como de 64bit.
- Linux Ubuntu 12.04, tanto la versión de 32bit como la de 64bit, incluso una versión ARM v7.
Qué funcionalidades nos brinda?
Tanto el main process como el renderer process ponen a nuestra diposición un conjunto de APIs para realizar diversas funcionalidades.
Ambos procesos nos brindan acceso al portapapeles (clipboard), a variables de entorno, a imágenes (png o jpg), a las propiedades de la pantalla (screen), a (shell) que sirve para manejar ficheros y URLs externas,
El main process por su lado tiene acceso a otras funcionalidades, como atajos de teclas, BrowserWindows, certificados, cookies, Debugger, diálogos nativos (ficheros, alertas), mensajes ipcMain, internalización (locales), menús, peticiones http/s, sesiones (cache, proxy, etc...), área de notificaciones del sistema.
El rendererer process tiene acceso a funcionalidades, como captura de audio y video, HTML File, mensajes ipcRenderer y remote, carga de URLs externas, etc...
Módulos más interesantes
BrowserWindow
Este módulo permite gestionar ventanas dentro de una aplicación Electron. Cada ventana es un proceso independiente conocido como el 'renderer'. Este proceso, como el 'main', tiene acceso a las APIs de Node.js
let win = new BrowserWindow({ propiedades })
Consejo: Utiliza una ventana invisible para ejecutar tareas de fondo. Esto se realiza con la propiedad show:false. Otra opción es crear una ventana sin bordes, barras de status, de herramientas, de título, etc... Esto se realiza con la propiedad frame:false.
Las ventanas pueden tener un fondo transparente, esto se realiza con la propiedad transparent:true.
El módulo BrowserWindow emite ciertos eventos, por ejemplo move cuando se mueve la ventana, resize cuando se redimensiona la ventana, close cuando se cierra la ventana, focus blur para cuando la ventana pierde o gana el foco, responsive cuando la ventana vuelve a responder a partir de un proceso que la dejó colgada y unresponsive cuando la ventana no responde debido a un proceso que la deja colgada.
Menu y MenuItem
Estos módulos permiten crear menús a medida. Exiten dos tipos de menús: los de aplicación (en la parte superior) y los de contexto (al hacer clic con el botón derecho). Por defecto Electron generará un menú mínimo para tu aplicación.
Explorador de archivos y navegador
El módulo shell permite acceder a ciertos elementos nativos como el explorador de archivos y el navegador por defecto.
El siguiente ejemplo abre un explorador de archivos en la ubicación "os.homedir".
const {shell} = require('electron')
const os = require('os')
const fileManagerBtn = document.getElementById('open-file-manager')
fileManagerBtn.addEventListener('click', (event) => {
shell.showItemInFolder(os.homedir())
})
Este otro ejemplo abre los links externamente, fuera de la app de Electron.
const {shell} = require('electron')
const exLinksBtn = document.getElementById('open-ex-links')
exLinksBtn.addEventListener('click', (event) => {
shell.openExternal('https://tutoriales.online')
})
Notificaciones
El módulo notification permite añadir notificaciones básicas en Electron. Utiliza el API de HTML5
Por ejemplo mostrar una simple notificación de texto:
const notification = {
title: 'Basic Notification',
body: 'Short message part'
}
const notificationButton = document.getElementById('basic-noti')
notificationButton.addEventListener('click', () => {
const myNotification = new window.Notification(notification.title, notification)
myNotification.onclick = () => {
console.log('Notification clicked')
}
})
Diálogos
El módulo dialog permite usar diálogos para abrir archivos, guardar archivos o mostrar mensajes de error o información.
Tray
El módulo tray permite crear un icono en el área de notificaciones del sistema operativo.
Drag and drop
Electron permite arrastrar ficheros y contenido hacia el sistema operativo.
IPC - Comunicación entre procesos
El módulo ipc permite enviar y recibir mensajes síncronos y asíncronos entre el proceso main y los procesos renderer.
Existe la versión ipcMain para el main y ipcRenderer para los renderer.
Obtener información - app, sistema, pantalla
El módulo app permite obtener cierta información, por ejemplo la ruta donde se ubica la app:
const {app, ipcMain} = require('electron')
app.getAppPath()
También se puede obtener la versión del módulo process:
const ver = process.versions.electron
Otro módulo interesante es os por ejemplo para obtener el directorio 'home':
const os = require('os')
const homeDir = os.homedir()
Por último, el módulo screen permite obtener información sobre el tamaño de ventana, cursores, etc:
const {screen} = require('electron')
const size = screen.getPrimaryDisplay().size
Copiar y pegar
Para ello se utiliza el módulo clipboard. Por ejemplo para copiar texto al portapapeles:
const {clipboard} = require('electron')
clipboard.writeText('mi texto')
Y para pegarlo:
const {clipboard} = require('electron')
const txt = clipboard.readText()
Imprimir
El módulo BrowserWindow tiene la propiedad webContents que permite imprimir a PDF por ejemplo:
Proceso renderer
const {ipcRenderer} = require('electron')
const printPDFBtn = document.getElementById('print-pdf')
printPDFBtn.addEventListener('click', (event) => {
ipcRenderer.send('print-to-pdf')
})
ipcRenderer.on('wrote-pdf', (event, path) => {
const message = `Wrote PDF to: ${path}`
document.getElementById('pdf-path').innerHTML = message
})
Proceso Main
const fs = require('fs')
const os = require('os')
const path = require('path')
const {BrowserWindow, ipcMain, shell} = require('electron')
ipcMain.on('print-to-pdf', (event) => {
const pdfPath = path.join(os.tmpdir(), 'print.pdf')
const win = BrowserWindow.fromWebContents(event.sender)
win.webContents.printToPDF({}, (error, data) => {
if (error) throw error
fs.writeFile(pdfPath, data, (error) => {
if (error) throw error
shell.openExternal(`file://${pdfPath}`)
event.sender.send('wrote-pdf', pdfPath)
})
})
})
Acceso a Media - audio, video
El módulo desktopCapturer permite acceder a cualquier medio, como audio, video, pantalla, etc. a partir de la API getUserMedia
Aplicación de ejemplo
En este ejemplo práctico vamos a construir desde cero una aplicación para obtener las cotizaciones en euros de las 3 criptomonedas más importantes según la web de coinmarketcap.com.
Para ello vamos a utilizar el editor Visual Studio Code, que un IDE muy ligero con autocompletado de código y resaltado de palabras clave, además de un interfaz del shell de comandos.
Nota: tener Node.js instalado, e instalar electron (globalmente) con npm install -g electron
Para empezar vamos a crear un directorio con la estructura siguiente:
tu-app/
├── package.json
├── app.js
└── index.html
Un ejemplo del fichero package.json sería:
{
"name" : "tu-app",
"version" : "0.1.0",
"main" : "app.js"
}
Luego habría que crear el fichero app.js, y el index.html
const {app, BrowserWindow} = require('electron')
const path = require('path')
const url = require('url')
// Hay que guardar una referencia global el objeto window, porque sino la ventana
// se cerrará cuando el garbage collector quite esa referencia
let win
function createWindow () {
// Crea la ventana del navegador
win = new BrowserWindow({width: 800, height: 600})
// y carga el index.html
win.loadURL(url.format({
pathname: path.join(__dirname, 'index.html'),
protocol: 'file:',
slashes: true
}))
// abre las herramientas de desarrollador
win.webContents.openDevTools()
// evento que se emite cuando la ventana se cierra
win.on('closed', () => {
// Quitar la referencia del objeto window, habitualmente se guardan
// las ventanas en un array si nuestra app tiene más de una
win = null
})
}
// Este método se invoca cuando Electron finaliza la inicialización y está preparado
// para crear browser windows.
// Algunas APIs sólo se pueden usar después de que se dispare este evento.
app.on('ready', createWindow)
// Salir cuando todas las ventanas se cierran
app.on('window-all-closed', () => {
// En macOS es habitual que la aplicación y su menú quede activo hasta que el
// usuario sale explícitamente con Cmd + Q
if (process.platform !== 'darwin') {
app.quit()
}
})
app.on('activate', () => {
// En macOS es común recrear una ventana cuando se hace clic en el
// icono del dock y no hay otras ventanas abiertas
if (win === null) {
createWindow()
}
})
// Incluir el resto de nuestra app
// Se pueden crear ficheros separados y invocarlos con require
Y el index.html es simplemente una página web
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Mi app</title>
</head>
<body>
<h1>Mi app</h1>
Texto de ejemplo:
</body>
</html>
Es recomendable realizar este curso online de Javascript desde cero ya que es la base de ElectronJS.
También es recomendable aprender Angular 6 para reaprovechar esta tecnología en Electron JS.
Vamos a completar el ejemplo hecho en electronjs que muestra la cotización de Bitcoin, Ethereum y Ripple, el Top 3 de las criptomonedas actuales haciendo una petición GET a la API de coinmarketcap.com.
Puedes revisar el proyecto de Github si lo deseas.
Al ser aplicaciones basadas en tecnología web, podemos usar librerías comunes como Bootstrap, jQuery, etc...
Un modelo básico habitual puede ser el proyecto siguiente:
- index.html
- styles.css
- app.js
- window.js
- package.json
El index.html
y el styles.css
definen el interfaz de la aplicación como si de una página web se tratara.
El window.js
implementa el método $() de jQuery que se invoca una vez la página se carga, ahí podemos usar código jQuery de toda la vida.
En el fichero app.js
definemos los métodos principales que hacen referencia a electron y las llamadas a window. Asimismo también podemos configurar un menú y ubicarlo en nuestra aplicación.
Así como hemos comentado, este sería el detalle de los ficheros del proyecto electron.
/* global $ */ const request = require('request'); // Run this function after the page has loaded $(() => { request('https://api.coinmarketcap.com/v1/ticker/?convert=EUR&limit=3', {json:true}, (error, res, body) => { if (error) { window.alert("No se ha podido conectar con la web de Coinmarketcap.com"); return console.log(error); } if (!error && res.statusCode===200) { for (item in body) { $('#name'+item).text(body[item].name); $('#price'+item).text(body[item].price_eur); const d = new Date(1000*body[item].last_updated); $('#lastupd'+item).text(d.toLocaleString()); } } }); });
const {app, BrowserWindow, Menu} = require('electron'); const path = require('path'); const url = require('url'); let window = null; const template = [ { role: 'window', label: 'Ventana', submenu: [ { role: 'minimize', label: 'Minimizar' }, { role: 'close', label: 'Cerrar' } ] }, ] const menu = Menu.buildFromTemplate(template); Menu.setApplicationMenu(menu); // Wait until the app is ready app.once('ready', () => { // Create a new window window = new BrowserWindow({ // Set the initial width to 400px width: 800, // Set the initial height to 500px height: 600, // Don't show the window until it ready, this prevents any white flickering show: false, // Don't allow the window to be resized. resizable: false }); // Load a URL in the window to the local index.html path window.loadURL(url.format({ pathname: path.join(__dirname, 'index.html'), protocol: 'file:', slashes: true })); // abre las herramientas de desarrollador //window.webContents.openDevTools(); // Show window when page is ready window.once('ready-to-show', () => { window.show(); }); });