Crea un plugin de Cordova para Electron

18 de abril de 2022
Última actualización: 1 de agosto de 2024
Tabla de contenidos

Post principal

Este post forma parte de este tutorial:

Si no has leido ese otro post, es posible que necesites leerlo antes de continuar con este. Aprenderás a:

Puedes ver el código fuente de este tutorial en GitHub:
https://github.com/adrian-bueno/multiplatform-cordova-plugin-example

Inicio

Este tutorial está escrito para cordova-electron 4.0.0

Vamos a implementar en Node.js los 4 métodos que definimos en nuestra interfaz de JavaScript www/CordovaPluginExample.js:

Crea un nuevo directorio electron dentro de src.

Después crea los ficheros CordovaPluginExample.js y package.json dentro de electron.

📁 cordova-plugin-example
└── 📁 src
    └── 📁 electron
        ├── 📄 CordovaPluginExample.js
        └── 📄 package.json

Añade la configuración mínima a este package.json:

{
  "name": "cordova-plugin-example",
  "version": "0.0.0",
  "main": "CordovaPluginExample.js",
  "cordova": {
    "serviceName": "CordovaPluginExample"
  }
}

Electron usa mínimo 2 procesos: el proceso main y el proceso renderer (podemos crear más procesos si queremos). El proceso main es el proceso principal y es un programa de Node.js. Los procesos renderer se crean cada vez que abrimos una BrowserWindow y se encarga de renderizar la aplicación web (son procesos de Chromium). Basicamente, el proceso main == Node.js, y el proceso renderer == Chromium. Más información

El código de src/electron se ejecuta en el proceso main.

Dependencias externas

Vamos a usar axios (un cliente HTTP basado en promesas y que funciona tanto en Node.js como el navegador) para obtener el valor actual del Bitcoin de https://api.coindesk.com/v1/bpi/currentprice.json .

Para usar dependencias externas, tenemos que añadirlas a las dependencias de src/electron/package.json:

{
  "name": "cordova-plugin-example",
  "version": "0.0.0",
  "main": "CordovaPluginExample.js",
  "cordova": {
    "serviceName": "CordovaPluginExample"
  },
  "dependencies": {
    "axios": "^0.26.0"
  }
}

Configuración del plugin.xml

La configuración de Electron en el plugin.xml es la más simple de todas:

<!-- Electron -->
<platform name="electron">
  <framework src="src/electron"/>
</platform>

No tenemos que especificar permisos ni ninguna otra cosa como en Android o iOS.

Estructura del código fuente de Electron

📁 cordova-plugin-example
├── 📁 src
│   ├── 📁 android
│   ├── 📁 ios
│   └── 📁 electron
│       ├── 📄 CordovaPluginExample.js
│       └── 📄 package.json
├── 📁 www
│   └── 📄 CordovaPluginExample.js
├── 📄 .gitignore
├── 📄 package.json
├── 📄 plugin.xml
└── 📄 README.md

Código fuente en Node.js

CordovaPluginExample.js

Abrir en GitHub

const fs = require("fs");
const path = require("path");
const os = require("os");
const axios = require("axios").default;

// Este es el ejemplo más sencillo.
// Devuelve el string "Hello {name}!"
// o "Hello!" si no se recibe ningún nombre.
function greeting(args) {
  const [name] = args;
  return name ? `Hello ${name}!` : "Hello!";
}

// Devuelve un número cada segundo, desde el valor
// del parámetro "seconds" hasta 0.
// NO ES POSIBLE IMPLEMENTARLO con cordova-electron 4.0.0+.
// cordova-electron 4.0.0+ no tiene implementada
// la funcionalidad de keepCallback que si está
// disponible en Android e iOS.
function countdownTimer(args) {
  // const [seconds] = args;
  throw "NOT_IMPLEMENTED";
}

// Escribe un fichero en la carpeta
// "Documents" del usuario
function writeFile(args) {
  const [fileName, text] = args;

  if (!fileName || !text) {
    throw "BAD_ARGS";
  }

  try {
    const dir = path.join(os.homedir(), "Documents");
    fs.mkdirSync(dir, { recursive: true });
    const filePath = path.join(dir, fileName);
    fs.writeFileSync(filePath, text);
  } catch (error) {
    console.error(error);
    throw "WRITE_ERROR"
  }
}

// Este ejemplo sirve solo para mostrar como usar
// la dependencia que hemos añadido en el package.json,
// en este caso axios.
// 
// Las funciones también pueden ser "async" y
// devolver promesas.
async function bitcoinCurrentPrice() {
  try {
    const response = await axios.get("https://api.coindesk.com/v1/bpi/currentprice.json")
    return response.data;
  } catch (error) {
    console.error(error);
    throw "REQUEST_ERROR";
  }
}

// Exporta las funciones, usa los mismos
// nombres que en las acciones de los métodos
// de www/CordovaPluginExample.js (el 4º parámetro):
//
// exports.writeFile = function (successCallback, errorCallback, fileName, text) {
//   exec(successCallback, errorCallback, PLUGIN_NAME, 'writeFile', [fileName, text]);
// };
module.exports = {
  greeting,
  countdownTimer,
  writeFile,
  bitcoinCurrentPrice,
};

Las funciones pueden devolver valores directamente o a través de una promesa.

Devolver varios valores (funcionalidad ‘keepCallback’)

Esta parte del tutorial no usa código oficial de cordova-electron (al menos por ahora).

Recientemente tuve que crear un plugin para Android y Electron en el que necesitaba detectar cuando se conectaba/desconectaba un dispositivo USB. Por lo que necesitaba la funcionalidad de keepCallback. Como esta funcionalidad no esta disponible por ahora en el código oficial de cordova-electron, tuve que crear un fork e implementarlo. Podeís ver el fork aquí (rama ‘feature/keep-callback’). También hice un pull request que está esperando la validación de los desarrolladores oficiales de cordova-electron. Si aprueban estos cambios o proponen otra implementación, actualizaré el tutorial.

Para usar este fork tenemos que reemplazar la versión 4.0.0 del package.json de nuestra app de Cordova por github:adrian-bueno/cordova-electron#feature/keep-callback .

Primero borra la plataforma de Electron:

npx cordova platform remove electron

Después vuelve a añadir la plataforma usando la URL del fork y la rama:

npx cordova platform add https://github.com/adrian-bueno/cordova-electron#feature/keep-callback

Deberías ver los siguiente en el package.json:

{
  ...

  "dependencies": {
    "cordova-electron": "github:adrian-bueno/cordova-electron#feature/keep-callback"
  }
  
  ...

}

Ahora si que podemos completar el código de la función countdownTimer.

Para esta funcionalidad decidí usar el $ para identificar a las funciones que pueden devolver más de un valor a lo largo del tiempo. Al implementarlo de esta forma no se rompe la funcionalidad actual y los plugins existentes no dejarían de funcionar.

Tenemos que hacer los siguientes cambios:

Edita www/CordovaPluginExample.js:

...

// Creamos una variable para detectar si estamos 
// ejecutando el código en Electron
const runningInElectron = navigator.userAgent.indexOf("Electron") >= 0;

// Si estamos en Electron, la acción debe terminar con un $
// Para el resto de plataformas continuamos dejando el mismo nombre
exports.countdownTimer = function (successCallback, errorCallback, seconds) {
  const action = runningInElectron ? "countdownTimer$" : "countdownTimer";
  exec(successCallback, errorCallback, PLUGIN_NAME, action, [seconds]);
};

...

Edita src/electron/CordovaPluginExample.js:

...

// Añade un $ al final del nombre de la función.
// Estas funciones tienen 3 parámetros:
// - callback OK
// - callback error
// - parámetros
//
// Las funciones de callback tienen
// la siguiente interfaz:
// - success(data: any, keepCallback: boolean): void
// - error(data: any): void
function countdownTimer$(success, error, args) {
  const [seconds] = args;

  let secondsLeft = seconds > 0 ? seconds : 10;

  function startTimeout() {
    const keepCallback = secondsLeft > 0;
    success(secondsLeft, keepCallback);
    if (keepCallback) {
      secondsLeft--;
      setTimeout(startTimeout, 1000);
    }
  }

  startTimeout();
}

...

module.exports = {
  greeting,
  countdownTimer$, // <= añade el $
  writeFile,
  bitcoinCurrentPrice,
};

Continua leyendo el post principal