Cómo construir una aplicación de escritorio Electron en JavaScript: multiproceso, SQLite, módulos nativos y otros puntos débiles comunes

Como marco para desarrollar aplicaciones de escritorio, Electron tiene mucho que ofrecer. Otorga acceso completo a la API y la ecosfera de Node. Se implementa en todos los principales sistemas operativos (con una única base de código). Y con su arquitectura basada en web, puede utilizar las últimas funciones de CSS para crear interfaces de usuario avanzadas.

Hay muchos artículos que abordan la puesta en marcha con Electron, pero pocos están dedicados al uso de SQLite o cómo realizar el multiproceso. Veremos cómo usar Electron para crear aplicaciones que manejen grandes cantidades de datos o ejecuten muchas tareas.

En particular, cubriremos:

  • Cómo funciona Electron (en resumen) y cómo su arquitectura afecta lo que podemos hacer
  • Multihilo
  • Usar bases de datos locales como SQLite o escribir en cualquier archivo dentro de una aplicación de Electron
  • Módulos nativos
  • Algunas trampas a tener en cuenta
  • Empaquetando una aplicación usando módulos nativos

Cómo funciona Electron - resumido

Vale la pena repetir los principios clave detrás de la arquitectura de Electron. Una aplicación de Electron consta de al menos dos procesos. El hilo principal es la entrada a su aplicación y hace todo el trabajo necesario para mostrar su proceso (o procesos) de renderizado a sus usuarios. Solo puede haber una instancia del proceso principal.

Los procesos de renderizado utilizan Chromium para renderizar su aplicación. Así como cada pestaña se ejecuta en su propio proceso, también lo hace cada renderizador. Se cargan mediante el método loadURL del constructor BrowserWindow, que debe apuntar a un archivo HTML local o remoto. Eso significa que la única forma de iniciar un proceso de renderizado es usar un archivo HTML como entrada.

Advertencias de la arquitectura de Electron

La simplicidad de Electron es uno de sus mayores activos. Su proceso principal realiza cualquier configuración necesaria y luego pasa un archivo HTML o URL al proceso de renderizado. Este archivo puede hacer todo lo que puede hacer una aplicación web normal, y está listo para comenzar desde allí.

Pero el hecho de que solo pueda haber un proceso principal hace que no quede claro cómo implementar subprocesos múltiples. La documentación de Electron implica que los procesos de renderizado están estrictamente diseñados para la tarea de renderizar IU (lo cual, como veremos, no es cierto).

Es importante saber que hacer cualquier cosa computacionalmente intensiva en el proceso principal ralentizará (o congelará) sus procesos de renderizado. Es fundamental que cualquier trabajo computacionalmente intensivo se retire del hilo principal. Es mejor dejarlo únicamente a la tarea de hacer todo lo necesario para iniciar sus procesos de renderizado. Dado que no podemos hacer un trabajo intensivo en el mismo proceso de renderizado que está renderizando la interfaz de la aplicación (ya que esto también afectará la UI), necesitamos otro enfoque.

Multihilo

Hay tres enfoques generales para el multihilo en Electron:

  • Utilizar trabajadores web
  • Bifurcar nuevos procesos para ejecutar tareas
  • Ejecutar procesos de renderizador (ocultos) como trabajadores

Trabajadores web

Dado que Electron está construido sobre Chromium, cualquier cosa que se pueda hacer en un navegador se puede hacer en un proceso de renderizado. Esto significa que puede utilizar trabajadores web para ejecutar tareas intensivas en subprocesos separados. La ventaja de este enfoque es la simplicidad y la retención del isomorfismo con una aplicación web.

Sin embargo, hay una advertencia muy importante: no puede utilizar módulos nativos. Técnicamente, puede, pero si lo hace, su aplicación se bloqueará. Este es un problema importante, ya que cualquier aplicación que necesite subprocesos múltiples también puede necesitar utilizar módulos nativos (como node-sqlite3).

Bifurcando nuevos procesos

Electron usa Node como tiempo de ejecución, lo que significa que tiene acceso completo a módulos integrados como clúster. Se pueden bifurcar nuevos procesos para ejecutar tareas, manteniendo el trabajo intensivo fuera del hilo principal.

El problema principal es que, a diferencia de los procesos de renderizado, los procesos secundarios no pueden utilizar métodos de la biblioteca de Electron. Esto le obliga a mantener un canal de comunicación con el proceso principal a través de IPC. Los procesos del renderizador pueden usar el módulo remoto para decirle al proceso principal que realice tareas solo principales sin este paso adicional.

Otro problema es que si está utilizando módulos ES o características TC39 de JavaScript, deberá asegurarse de ejecutar versiones transpiladas de sus scripts. También deberá incluirlos en su aplicación empaquetada. Este problema afecta a cualquier aplicación de nodo que bifurque procesos, pero agrega otra capa de complejidad a su proceso de compilación. También puede resultar complicado al equilibrar las demandas de empaquetar su aplicación con el uso de herramientas de desarrollo que utilizan características como la recarga en vivo.

Usar procesos de renderizado como hilos de trabajo

Los procesos de renderizado se tratan convencionalmente como si se utilizaran para renderizar su interfaz de usuario. Sin embargo, no están obligados a esta única tarea. Pueden ocultarse y ejecutarse en segundo plano configurando el indicador show pasado a BrowserWindow.

Hacer esto tiene muchas ventajas. A diferencia de los trabajadores web, tiene la libertad de utilizar módulos nativos. Y a diferencia de los procesos bifurcados, aún puede usar la biblioteca de electrones para decirle al proceso principal que haga cosas como abrir un diálogo o crear notificaciones del sistema operativo.

Un desafío al usar Electron es IPC. Si bien es simple, requiere una gran cantidad de texto estándar e impone la dificultad de depurar una gran cantidad de oyentes de eventos. También es otra cosa que tienes que probar unitariamente. Al utilizar un proceso de renderizado como un hilo de trabajo, puede evitar esto por completo. Al igual que lo haría con un servidor, puede escuchar en un puerto local y recibir solicitudes, lo que le permite utilizar herramientas como GraphQL + React Apollo. También puede utilizar websockets para la comunicación en tiempo real. Otra ventaja es que no necesita usar ipcRenderer, y puede mantener sus aplicaciones web y Electron isomórficas (si desea usar una base de código compartida para una aplicación de escritorio y web).

Para casos de uso avanzados, este enfoque se puede combinar con la agrupación en clústeres para obtener lo mejor de todos los mundos. El único inconveniente es que deberá proporcionar un archivo HTML como entrada para los procesos del renderizador de trabajo (que se siente como una especie de truco).

Cómo usar SQLite (o cualquier cosa en la que necesite escribir)

Hay varios enfoques para la gestión del estado que no requieren módulos nativos. Por ejemplo, manejar todo su estado en el contexto de un renderizador con Redux.

Sin embargo, si necesita manejar grandes cantidades de datos, esto no será suficiente. En particular, veremos cómo usar SQLite en una aplicación de Electron.

Para implementar su aplicación Electron, primero deberá empaquetarla. Hay varias herramientas disponibles para hacerlo, la más popular es Electron Builder. Electron utiliza el formato de archivo ASAR para agrupar su aplicación en un solo archivo sin comprimir. Los archivos ASAR son de solo lectura, lo que significa que no puede escribir ningún dato en ellos. Esto significa que no puede incluir su base de datos en su archivo ASAR junto con el resto de su código (en el generador de electrones, esto estaría en "archivos").

En su lugar, incluya su base de datos en el directorio de recursos de su paquete electrónico. La estructura de archivos de una aplicación Electron empaquetada y dónde colocar su base de datos se puede ver a continuación:

El archivo ASAR empaquetado llamado app.asar existe en ./Contents/Resources. Puede colocar su base de datos, o cualquier archivo en el que desee escribir pero incluirlo en su aplicación empaquetada, en el mismo directorio. Esto se puede lograr con Electron Builder usando la configuración "extraResources".

Otro enfoque es crear una base de datos en otro directorio por completo. Pero deberá tener en cuenta la eliminación de este archivo en todas las plataformas si los usuarios deciden desinstalar su aplicación.

Empaquetado con módulos nativos

La gran mayoría de los módulos de nodo son scripts escritos en JavaScript. Los módulos nativos son módulos escritos en C ++ que tienen enlaces para usar con Node. Actúan como una interfaz para otras bibliotecas escritas en C / C ++ y, por lo general, se configuran para compilarse después de la instalación.

Como módulos de bajo nivel, deben compilarse para arquitecturas y sistemas operativos de destino. Un módulo nativo compilado en una máquina con Windows no funcionará en una máquina con Linux, aunque sí lo haría un módulo normal. Este es un problema para Electron, ya que eventualmente tendremos que empaquetar todo en un ejecutable .dmg (OSX), .exe (Windows) o .deb (Linux).

Las aplicaciones electrónicas que utilizan módulos nativos deben empaquetarse en el sistema al que se dirigen. Como querrá automatizar este proceso en una canalización de CI / CD, deberá crear sus dependencias nativas antes de empaquetar. Para lograr esto, puede utilizar una herramienta desarrollada por el equipo de Electron llamada electron-rebuild.

Si está desarrollando un proyecto de código abierto no comercial, puede usar TravisCI (Linux, OSX) y Appveyor (Windows) para construir, probar e implementar automáticamente su aplicación de forma gratuita.

La configuración para esto puede ser complicada si tiene pruebas de integración, ya que deberá instalar ciertas dependencias para que funcionen las pruebas sin cabeza. Puede encontrar una configuración de ejemplo para OSX y Linux con TravisCI aquí, y una configuración de Appveyor de ejemplo aquí (estos ejemplos se basan en la configuración en el proyecto electron-react-boilerplate, con la adición de OSX y la implementación).

Gotchas

Cuando su aplicación Electron está empaquetada, algunas propiedades integradas de Node relacionadas con las rutas pueden no comportarse como usted esperaría y no se comportarán como lo hacen cuando ejecuta el binario prediseñado para servir su aplicación.

Variables como __dirname, __filename y métodos como process.cwd no se comportarán como se esperaba en una aplicación empaquetada (vea los problemas aquí, aquí y aquí). En su lugar, utilice app.getAppPath.

Una nota final sobre el embalaje

Mientras desarrolla una aplicación de Electron, es posible que desee utilizar los modos de producción (que ofrece código empaquetado con el binario precompilado) y de desarrollo (utilizando webpack-dev-server o webpack-serve para ver sus archivos).

Para conservar la cordura, cree y proporcione sus paquetes desde el mismo directorio que su código fuente. Esto significa que cuando selecciona estos archivos para empaquetarlos, cualquier supuesto de ruta de archivo permanece consistente en estos modos y su paquete.

Como mínimo, su proceso principal deberá apuntar a la ruta de archivo del archivo HTML de sus procesos de renderizado. Si mueve este archivo a otro directorio como parte de su proceso de compilación, se verá obligado a mantener los supuestos de la estructura del archivo y esto puede convertirse rápidamente en otra capa de complicación que debe mantener.

La depuración de problemas relacionados con rutas de archivo incorrectas en una aplicación empaquetada es en gran medida un caso de prueba y error.

Resumen

Hay varios enfoques para el multihilo en Electron. Los trabajadores web son convenientes, pero carecen de la capacidad de utilizar módulos nativos. La bifurcación de nuevos procesos funciona como lo haría en Node, pero la falta de capacidad para usar la biblioteca de Electron obliga al uso de IPC para tareas comunes y puede complicarse rápidamente. El uso de procesos de renderizado como trabajadores otorga todo el poder de todas las herramientas de servidor de Node disponibles como reemplazo de la comunicación a través de IPC, al tiempo que conserva el acceso a los módulos y métodos nativos de la biblioteca de renderizado de Electron.

Como Electron empaqueta archivos en un archivo ASAR de solo lectura, no se puede incluir ningún archivo en el que necesitemos escribir (como una base de datos SQLite). En su lugar, estos se pueden colocar en el directorio de Recursos donde permanecerán en la aplicación empaquetada.

Por último, tenga en cuenta el hecho de que en una aplicación empaquetada, algunas propiedades de Node no se comportan como cabría esperar. Y para mayor claridad, haga coincidir la estructura de archivos de su aplicación empaquetada con su código fuente.