Crea la capa de Ionic Native para un plugin de Cordova
En este post aprenderemos como crear la capa de @awesome-cordova-plugins para hacer más sencillo el uso de nuestro plugin de Cordova. Esta simplificación se consigue mapeando las funciones de callback del plugin de Cordova a Promesas y Observables.
@awesome-cordova-plugins se llamaba antes @ionic-native.
El proyecto ha cambiado solo el nombre, la funcionalidad continua igual.
Este post es la continuación del siguiente tutorial:
Por lo que la creación de esta capa se basará en el plugin que construimos en ese tutorial.
Puedes encontrar el código fuente de este tutorial en GitHub:
https://github.com/adrian-bueno/multiplatform-cordova-plugin-example
(en packages > awesome-cordova-plugins-example).
Empezaremos primero definiendo como abordar la creación de esta capa dependiendo de si nuestro plugin será público o privado. Después veremos como se escribe esta capa usando TypeScript.
Mi plugin será público
Si tu plugin será público, simplemente crea un fork del repositorio oficial de @awesome-cordova-plugins. Luego clona tu fork:
git clone https://github.com/<tu-nombre-de-usuario>/awesome-cordova-plugins.git
Una vez lo hayas clonado, crea un nuevo directorio con el nombre de tu plugin
dentro de src/@awesome-cordova-plugins/plugins
.
Después crea un fichero index.ts
dentro de este nuevo directorio.
Veremos que escribir en él, en la sección: Código de la capa.
Una vez que tengas el código listo, crea un pull request al repositorio oficial. Cuando sea aceptado, tu capa estará disponible en npm
con el nombre de paquete @awesome-cordova-plugins/<nombre-del-directorio-de-tu-plugin>
.
Mi plugin será privado
Para construir un plugin privado tenemos 2 opciones:
-
Creamos un fork del repositorio oficial. Borramos todos los directorios dentro de
src/@awesome-cordova-plugins/plugins
y creamos nuevos directorios para todas nuestras capas de plugins. Luego subimos los paquetes generados a nuestro repositorio privado denpm
. -
(Esta opción es un preferencia personal) Creamos un fork del repositorio oficial y adaptamos el código para que solo contenga una capa para un plugin. Además de borrar todos los ficheros que no consideremos necesarios.
Da igual la opción que elijamos, pues con ambas opciones tendremos que ir actualizando el código de los scripts de construcción con el código del repositorio oficial.
Os voy a mostrar como he adaptado el repositorio oficial para que solo contenga una capa en vez de varias y solo genere un paquete.
También podeis ahorraros leer esto y usar directamente mi código como base para el vuestro. Ver en GitHub.
Primero, clona el repositorio oficial y cambiale el nombre:
git clone --depth=1 https://github.com/danielsogl/awesome-cordova-plugins.git awesome-cordova-plugins-example
Borra los siguientes directorios y ficheros:
📁 .git
📁 .github
📁 .husky
📁 docs
📁 scripts
├── 📁 docs
├── 📁 tasks
│ └── 📄 publish.ts
├── 📁 templates
└── 📄 logger.ts **
📁 src
└── 📁 @awesome-cordova-plugins
📄 .editorconfig **
📄 .eslintignore **
📄 .eslintrc **
📄 .gitbook.yml
📄 .prettierignore **
📄 CHANGELOG.md **
📄 CODE_OF_CONDUCT.md
📄 DEVELOPER.md
📄 gulpfile.js
📄 prettier.config.js **
📄 LICENSE **
📄 renovate.json
📄 tsconfig.core.json
Puedes mantener los ficheros marcados con
**
, pero adaptalos a tus necesidades y gustos. Si borras el ficherologger.ts
tendrás que usarconsole.log
en los ficheros que usen el logger.
Borra el directorio
.git
y crea un nuevo repositorio.
Borra la propiedad paths
del fichero tsconfig.json
y edita el valor de la propiedad include
:
{
"compilerOptions": {
"baseUrl": ".",
"declaration": true,
"stripInternal": true,
"experimentalDecorators": true,
"emitDecoratorMetadata": true,
"noImplicitAny": true,
"module": "es2015",
"moduleResolution": "node",
"outDir": "./dist",
"rootDir": "src",
"target": "es5",
"skipLibCheck": true,
"lib": ["es2017", "dom"],
"inlineSources": true,
"inlineSourceMap": true
},
"include": ["src/**/*.ts"], <---
"angularCompilerOptions": {
"genDir": "aot"
}
}
Borra también paths
del fichero scripts/tsconfig.json
:
{
"compilerOptions": {
"module": "commonjs",
"target": "es5",
"moduleResolution": "node",
"noImplicitAny": false,
"lib": ["es6"]
},
"exclude": ["node_modules"]
}
Haz limpieza en el package.json
e instala @awesome-cordova-plugins/core
como una dependencia de desarrollo.
Cambia también el nombre y la versión.
{
"name": "awesome-cordova-plugins-example",
"version": "1.0.0",
"description": "Native plugin wrapper for cordova-plugin-example",
"scripts": {
"build:esm": "ts-node -P scripts/tsconfig.json scripts/tasks/build-esm",
"build:es5": "ts-node -P scripts/tsconfig.json scripts/tasks/build-es5",
"build:ngx": "ts-node -P scripts/tsconfig.json scripts/tasks/build-ngx",
"build": "npm run build:esm && npm run build:ngx && npm run build:es5",
"prebuild": "rimraf -rf dist"
},
"peerDependencies": {
"@awesome-cordova-plugins/core": "^5.1.0",
"rxjs": "^5.5.0 || ^6.5.0"
},
"devDependencies": {
"@angular/common": "11.2.14",
"@angular/compiler": "11.2.14",
"@angular/compiler-cli": "11.2.14",
"@angular/core": "11.2.14",
"@awesome-cordova-plugins/core": "^5.41.0",
"@types/cordova": "0.0.34",
"@types/fs-extra": "9.0.13",
"@types/lodash": "4.14.181",
"@types/node": "16.11.26",
"@types/rimraf": "3.0.2",
"@types/webpack": "5.28.0",
"fs-extra": "10.0.1",
"lodash": "4.17.21",
"rimraf": "3.0.2",
"rollup": "2.70.1",
"rxjs": "6.6.7",
"terser-webpack-plugin": "5.3.1",
"ts-node": "10.7.0",
"typescript": "4.1.6",
"unminified-webpack-plugin": "3.0.0",
"webpack": "5.71.0",
"winston": "3.7.2",
"zone.js": "0.11.5"
}
}
Lo siguiente que tenemos que hacer, es reemplazar en todos los ficheros TypeScript
de la carpeta scripts
:
dist/@awesome-cordova-plugins/**
pornode_modules/@awesome-cordova-plugins/**
.src/@awesome-cordova-plugins/plugins
porsrc
.
También debemos cambiar el valor de la variable PLUGIN_PATHS
de scrits/build/helpers.ts
:
export const PLUGIN_PATHS = [join(ROOT, 'src', 'index.ts')];
Y borrar transpileNgxCore()
de scripts/tasks/build-ngx.ts
, ya que hemos borrado el paquete core
de src
.
Por último, crea un nuevo fichero TypeScript dentro de scripts/tasks
con el nombre build-packagejson.ts
:
import { readFileSync, writeFileSync } from 'fs-extra';
import path = require('path');
import { ROOT } from '../build/helpers';
const packageJson = JSON.parse(readFileSync(path.join(ROOT, "package.json")).toString());
const newPackageJson = {
name: packageJson.name,
version: packageJson.version,
module: packageJson.module,
main: packageJson.main,
peerDependencies: packageJson.peerDependencies
}
writeFileSync(path.join(ROOT, "dist/package.json"), JSON.stringify(newPackageJson, null, 4));
Este script crea un nuevo package.json
dentro del directorio dist
.
Copia los datos del package.json
de la raíz y ‘borra’ los datos que no son necesarios.
Añade las propiedades que necesites a la variable newPackageJson
.
Actualiza el script build
del package.json
de la raíz.
{
"build:packagejson": "ts-node -P scripts/tsconfig.json scripts/tasks/build-packagejson",
"build": "npm run build:esm && npm run build:ngx && npm run build:es5 && npm run build:packagejson",
}
Antes de ejecutar este script, tenemos que escribir el código de nuestra capa en src/index.ts
.
Código de la capa
index.ts
(Open on GitHub)
import { Injectable } from '@angular/core';
import { Cordova, Plugin, AwesomeCordovaNativePlugin } from '@awesome-cordova-plugins/core';
import { Observable } from 'rxjs';
// Vamos a definir primero 2 interfaces para
// el objeto con el precio de Bitcoin
// Por ahora no sé si se pueden definir
// interfaces en ficheros separados.
// He tenido problemas al hacerlo, por lo menos
// con el script 'build-ngx'.
export interface BPI {
code: string;
description: string;
rate: string;
rate_float: number;
symbol: string;
}
export interface BitcoinCurrentPriceResponse {
bpi: {
EUR: BPI;
GBP: BPI;
USD: BPI;
},
chartName: string;
disclaimer: string;
time: {
updated: string;
updatedISO: string;
updateduk: string;
}
}
// Necesitamos 2 decoradores, @Plugin e @Injectable
//
// Para las propiedades de @Plugin, recuerda
// lo que escribimos en el fichero plugin.xml:
//
// <!-- www -->
// <js-module name="CordovaPluginExample" src="www/CordovaPluginExample.js">
// <clobbers target="cordova.plugins.CordovaPluginExample" />
// </js-module>
//
// pluginName == js-module.name
// pluginRef == clobbers.target
@Plugin({
pluginName: 'CordovaPluginExample',
plugin: 'cordova-plugin-example',
pluginRef: 'cordova.plugins.CordovaPluginExample',
repo: 'https://github.com/adrian-bueno/multiplatform-cordova-plugin-example',
platforms: ['Android', 'iOS', 'Electron']
})
@Injectable()
export class AwesomeCordovaPluginExample extends AwesomeCordovaNativePlugin {
// Usa el decorador @Cordova en cada método.
// Como en nuestro fichero JavaScript del plugin (www/CordovaPluginExample.js)
// pusimos primero las funciones de callback
// y después los parametros de entrada,
// ahora tenemos que declarar la propiedad
// 'callbackOrder' como 'reverse'.
@Cordova({
callbackOrder: 'reverse'
})
greeting(name?: string): Promise<string> { return null; }
// Si un método puede devolver más de un valor
// a lo largo del tiempo, declara la propiedad
// 'observable' como 'true'.
@Cordova({
observable: true,
callbackOrder: 'reverse'
})
countdownTimer(seconds?: number): Observable<number> { return null; }
// No tenemos que añadir ninguna lógica dentro
// de los métodos, simplemente devuelve null.
@Cordova({
callbackOrder: 'reverse'
})
writeFile(fileName: string, text: string): Promise<string> { return null; }
@Cordova({
callbackOrder: 'reverse'
})
bitcoinCurrentPrice(): Promise<BitcoinCurrentPriceResponse> { return null; }
}
Lee la documentación oficial para ver más opciones y detalles de @Plugin
y @Cordova
.
Prueba la capa en una app de Cordova
Esta capa funciona con:
- Angular
- AngularJS
- React
- ES2015+/TypeScript
- VanillaJS
Lee la documentación oficial para más detalles y ejemplos de código.
Voy a usar Angular (+ Cordova) para probar el código.
# Instala el CLI de Angular
npm i -g @angular/cli
# Crea una app de Angular
ng new <app-name>
cd <app-name>
Cambia el destino de compilación de Angular a www
y añade ./
como baseHref
en el fichero angular.json
:
{
...
"architect": {
"build": {
"builder": "@angular-devkit/build-angular:browser",
"options": {
"baseHref": "./", <---
"outputPath": "www", <---
"index": "src/index.html",
"main": "src/main.ts",
...
}
Copia el fichero config.xml
de la app que creamos en el tutorial principal. Puedes copiarlo de mi repositorio de GitHub.
Instala el CLI de Cordova y las plataformas de Cordova que vayamos a usar:
# Instala el CLI de Cordova
npm i -D cordova
# Crea el directorio www para
# que Cordova no se queje de que
# la app no es una app de Cordova
# cuando instalemos las plataformas
mkdir www
# Añade la plataforma Android
npx cordova platform add android
# Añade la plataforma iOS (solo si estás usando macOS)
npx cordova platform add ios
# Añade la plataforma Electron
npx cordova platform add electron@3.0.0
# O si quieres usar mi fork de cordova-electron
# para tener la funcionalidad 'keepCallback'
npx cordova platform add https://github.com/adrian-bueno/cordova-electron#feature/keep-callback
Añade
www
,plugins
yplatforms
al fichero.gitignore
.
Instala @awesome-cordova-plugins/core
:
npm i @awesome-cordova-plugins/core
Instala la capa que hemos creado:
npm i <path-to-wrapper>/awesome-cordova-plugins-example/dist
Instala el plugin:
npx cordova plugin add <path-to-plugin>/cordova-plugin-example
Consejo: Mueve todas las
dependencies
adevDependencies
(package.json) si vas a construir una app de Electron. Vas a reducir el tamaño de la app generada. Las dependencias de Angular solo son necesarias para compilar la app y no se usan mientras la app se está ejecutando.
Añade el script cordova.js
al final del index.html
:
<script src="cordova.js"></script>
Borra el código por defecto de app.component.html
y app.component.ts
.
Añade la clase de la capa a la lista de providers
en app.module.ts
:
// En Angular debemos importar la ruta /ngx
import { AwesomeCordovaPluginExample } from 'awesome-cordova-plugins-example/ngx';
@NgModule({
// ...
providers: [AwesomeCordovaPluginExample],
// ...
})
export class AppModule { }
Si tu plugin es público, su nombre de paquete será
@awesome-cordova-plugins/example
en vez deawesome-cordova-plugins-example
.
Ahora vamos a crear unos botones en AppComponent, como hicimos en el tutorial principal.
Empezamos con app.component.ts
:
import { Component } from '@angular/core';
import { AwesomeCordovaPluginExample } from 'awesome-cordova-plugins-example/ngx'
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.scss']
})
export class AppComponent {
constructor(private awesomeCordovaPluginExample: AwesomeCordovaPluginExample) {}
greeting(name?: string) {
this.awesomeCordovaPluginExample.greeting(name)
.then(res => console.log(res))
.catch(error => console.error(error));
}
countdown(seconds: number) {
this.awesomeCordovaPluginExample.countdownTimer(seconds)
.subscribe(sec => console.log(sec));
}
writeFile(fileName: string, text: string) {
this.awesomeCordovaPluginExample.writeFile(fileName, text)
.then(res => console.log(res))
.catch(error => console.error(error));
}
bitcoinCurrentPrice() {
this.awesomeCordovaPluginExample.bitcoinCurrentPrice()
.then(res => console.log(res))
.catch(error => console.error(error));
}
}
app.component.html
:
<button (click)="greeting('World!')">
Greeting "Hello World!"
</button>
<button (click)="greeting()">
Greeting "Hello!"
</button>
<hr>
<button (click)="countdown(12)">
Countdown (12)
</button>
<hr>
<button (click)="writeFile('cordova-plugin-example.txt', 'Hello there 👋')">
Write file
</button>
<hr>
<button (click)="bitcoinCurrentPrice()">
Bitcoin current price
</button>
Nuestra app de prueba esta lista. Ahora podemos compilarla y ejecutarla en una de las plataformas de Cordova.
npm run build && npx cordova run android
npm run build && npx cordova run ios
npm run build && npx cordova run electron
Nota: No se puede ejecutar
cordova-electron@3.0.0
con Node.js 16, usa la versión 14.
Deberías ver los resultados del plugin en la consola al pulsar los botones. Si no sabes como ver los logs, puedes leer como hacerlo en el tutorial principal.