Crear una aplicación de escritorio con Electron.js puede ser muy simple o muy complejo. Si queremos usar React por ejemplo seguramente vamos a terminar usando Babel y si usamos Babel seguro vamos a querer usar Wepback. Esto puede terminar en muchas configuraciones que hay que realizar, lo cual puede ser muy tedioso.
Con Next.js podemos hacer aplicaciones web con React sin configurar nada (o casi nada). Entonces, ¿por qué no hacer algo similar en Electron? Podemos combinar estas tecnologías para hacer eso mismo. Veamos cómo con un pequeño proyecto.
Iniciar el proyecto
Lo primero que vamos a hacer es iniciar nuestro proyecto, para eso vamos a usar el comando:
yarn init --yes
Luego de generar el package.json
vamos a instalar estas dependencias:
yarn add -E electron next react react-dom
Luego vamos a agregar estos scripts a nuestro package.json
:
"scripts": {
"dev": "electron index.js",
"build": "next build",
"start": "NODE_ENV=production electron index.js"
}
De esta forma vamos a poder usar yarn dev
para correr nuestra aplicación en modo desarrollo, yarn build
para generar nuestro código listo para producción y yarn start
para correr la aplicación en modo producción usando los archivos generados antes (esto no es para generar el binario de Electron).
Proceso principal
Electron usa dos tipos de procesos, el principal (main) y la UI (renderer). Empecemos a crear el código del proceso main.
// cargamos los módulos de electron que vamos a usar
const { app, BrowserWindow, ipcMain } = require('electron');
// cargamos la función para crear un servidor HTTP
const { createServer } = require('http');
// cargamos Next.js
const next = require('next');
// verificamos si estamos en modo desarrollo
const dev = process.env.NODE_ENV !== 'production';
// crearmos nuestra app de Next.js
const nextApp = next({ dev });
// obtenermos la función para manejar peticiones a Next.js
const handler = nextApp.getRequestHandler();
// en esta variable vamos a guardar la instancia de nuestra UI
let win;
function createWindow() {
// iniciamos la app de Next.js
nextApp
.prepare()
.then(() => {
// una vez lista creamos un servidor HTTP que use nuestro
// handler para ruteas todas las peticiones HTTP
// que reciba Next.js
const server = createServer((req, res) => {
// si recibimos una petición por fuera de Electron
// respondemos con un 404
if (req.headers['user-agent'].indexOf('Electron') === -1) {
res.writeHead(404);
res.end();
return;
}
res.setHeader('Access-Control-Request-Method', 'GET');
// solo permitimos peticiones GET
if (req.method !== 'GET') {
res.writeHead(405);
res.end('Method Not Allowed');
return;
}
// dejamos que Next maneje las peticiones
return handler(req, res);
});
// empezamos a escuchar el puerto 3000 con el servidor HTTP
server.listen(3000, (error) => {
if (error) throw error;
// una vez iniciamos el servidor creamos una nueva ventana
win = new BrowserWindow({
height: 768,
width: 1024,
});
// y abrimos la URL local del servidor HTTP que creamos
win.loadURL('http://localhost:3000');
// abrimos las herramientas de desarrollo
if (dev) {
win.webContents.openDevTools();
}
win.on('close', () => {
// cuando el usuario cierra la ventana borramo `win`
// y paramos el servidor HTTP
win = null;
server.close();
});
});
});
}
// una vez la app esté lista iniciamos una ventana nueva
app.on('ready', createWindow)
// si todas las ventanas se cerraros matamos la aplicación
app.on('window-all-closed', () => {
if (process.platform !== 'darwin') {
app.quit();
}
})l
// cuando se activa la app volvemos a abrir una ventana nueva
// (algo solo para Mac)
app.on('activate', () => {
if (win === null) {
createWindow();
}
});
// acá vamos a usar el módulo `ipcMain` para recibir mensajes desde
// nuestro proceso de UI y reenviar ese mensaje a quién lo envió
ipcMain.on('message', (event, message) => {
event.sender.send('message', message);
});
Ese va a ser nuestro proceso principal, con eso vamos a hacer que al abrir la aplicación se cree una nueva ventana (UI) y empecemos a usar Next.js para compilar nuestro código de React.
Proceso de la UI
Ahora podemos crear nuestra primera vista de la UI, algo bastante sencillo, en una carpeta llamada pages
. En la raíz del proyecto colocamos un archivo index.js
con el siguiente código:
// importamos React.Component, Next.js por defecto importa React
// para soportar JSX, así que solo necesitamos traernos Component
import { Component } from 'react';
// importamos los módulos de Electron que vamos a usar
import { ipcRenderer } from 'electron';
// creamos y exportamos el componente de la home
export default class HomePage extends Component {
// iniciamos el estado del componente
state = {
input: '',
message: null,
}
componentDidMount() {
// una vez el componente se monta vamos a empezar a escuchar
// el evento `message` que recibimos del proceso principal
// mediante el módulo `ipcRenderer`
ipcRenderer.on('message', this.handleMessage)
}
componentWillUnmount() {
// antes de desmontar el componente dejamos de escuchar
// el evento `message`
ipcRenderer.removeListener('message', this.handleMessage)
}
handleMessage = (event, message) => {
// acá recibimos el mensaje del proceso **main** y lo
// guardamos en el estado interno del componente
this.setState({ message })
}
handleChange = event => {
// acá guardamos el contenido de un input en el estado
this.setState({ input: event.target.value })
}
handleSubmit = event => {
// acá evitamos el submit del formulario y enviamos
// al proceso **main** el contenido del input
event.preventDefault();
ipcRenderer.send('message', this.state.input)
}
render() {
// por último hacemos render de un H1, si hay un mensaje
// desde el proceso **main** lo mostramos, luego el formulario
// y por último estilizamos el H1.
return (
<div>
<h1>Hola Electron!</h1>
{this.state.message &&
<p>{this.state.message}</p>
}
<form onSubmit={this.handleSubmit}>
<input type="text" onChange={this.handleChange} />
</form>
<style jsx>{`
h1 {
color: red;
font-size: 50px;
}
`}</style>
</div>
)
}
}
Configurando Next.js
Con esto ya tenemos nuestra primer vista. Si ahora tratamos de iniciar nuestra aplicación con yarn dev
, vamos a ver que al entrar nos da un error. Esto es porque por defecto Next.js no soporta Electron y por lo tanto no se pueden usar los módulos de éste como ipcRenderer
.
Vamos a solucionar esto creando un pequeño archivo llamado next.config.js
en la raíz de nuestro proyecto con este contenido:
// en este archivo vamos
module.exports = {
webpack(config) {
config.target = 'electron-renderer';
return config;
},
};
Este archivo nos permite modificar o extender la configuración interna de Webpack que posee Next.js. Gracias a eso podemos configurar que Webpack genere el código para el proceso renderer de Electron.
Corriendo la aplicación
Ahora sí, si usamos el comando yarn dev
vamos a ver como se abre nuestra aplicación de Electron, compila Next.js y se abre una ventana con la UI que renderizamos en pages/index.js
.
Si escribimos en el <input />
y luego enviamos el mensaje va a aparecer entre el título y el formulario, esto es porque el mensaje se envió mediante IPC al proceso main y de vuelta al proceso renderer (nuestra UI) y así lo pudimos mostrar.
Si corremos yarn build
podemos hacer que Next.js genere los archivos para producción. Luego simplemente al correr next start
podemos iniciar nuestra aplicación usando estos archivo pregenerados y que abra extremadamente rápido.
Conclusiones
Como vimos podemos combinar estas dos tecnologías para crear aplicaciones de escritorio muy poderosas y de forma fácil. Luego podemos ir integrando otras tecnologías según necesitemos, como Redux, o usar el método getInitialProps
que nos da Next.js para obtener datos de algún servicio HTTP y comunicarnos con el resto del mundo.