Saltar a contenido

Apache Cordova / PhoneGap Cheatsheet

■h1 títuloApache Cordova / PhoneGap - Híbrido Mobile App Desarrollo significa/h1 contacto "Clase de inscripción" Apache Cordova (antes PhoneGap) es un marco de desarrollo de aplicaciones móviles que permite a los desarrolladores construir aplicaciones móviles nativas usando HTML, CSS y JavaScript. Proporciona acceso a APIs de dispositivo nativo a través de plugins JavaScript. ▪/p] ■/div titulada

########################################################################################################################################################################################################################################################## Copiar todos los comandos
########################################################################################################################################################################################################################################################## Generar PDF seleccionado/button

■/div titulada ■/div titulada

Cuadro de contenidos

Instalación

Prerrequisitos

# Install Node.js (version 12 or later)
# Download from nodejs.org

# Verify Node.js installation
node --version
npm --version

# Install Java Development Kit (JDK) 8 or later
# Download from Oracle or use OpenJDK

# Install Android Studio (for Android development)
# Download from developer.android.com

# Install Xcode (for iOS development, macOS only)
# Download from Mac App Store

# Set environment variables
export ANDROID_HOME=$HOME/Library/Android/sdk
export PATH=$PATH:$ANDROID_HOME/tools
export PATH=$PATH:$ANDROID_HOME/platform-tools

Cordova CLI Instalación

# Install Cordova CLI globally
npm install -g cordova

# Verify installation
cordova --version

# Check requirements for platforms
cordova requirements

# Install platform-specific tools
# For Android
npm install -g gradle

# For iOS (macOS only)
npm install -g ios-deploy

PhoneGap CLI (Alternative)

# Install PhoneGap CLI (Adobe's distribution)
npm install -g phonegap

# Verify installation
phonegap --version

# PhoneGap Build service (deprecated)
# Use Cordova CLI for new projects

Comienzo

Crear un nuevo proyecto

# Create new Cordova project
cordova create MyApp com.example.myapp "My App"

# Navigate to project directory
cd MyApp

# Add platforms
cordova platform add android
cordova platform add ios

# Add plugins
cordova plugin add cordova-plugin-device
cordova plugin add cordova-plugin-camera

# Build the app
cordova build

# Run on device/emulator
cordova run android
cordova run ios

Plantillas del proyecto

# Create with specific template
cordova create MyApp com.example.myapp "My App" --template hello-world

# Create with custom template
cordova create MyApp com.example.myapp "My App" --template https://github.com/user/template.git

# Create blank project
cordova create MyApp com.example.myapp "My App" --template blank

# Create with TypeScript template
cordova create MyApp com.example.myapp "My App" --template cordova-template-typescript

Estructura del proyecto

Estructura básica

MyApp/
├── config.xml           # Cordova configuration
├── package.json         # Node.js dependencies
├── www/                 # Web assets
│   ├── index.html       # Main HTML file
│   ├── css/            # Stylesheets
│   ├── js/             # JavaScript files
│   └── img/            # Images
├── platforms/          # Platform-specific code (generated)
│   ├── android/
│   └── ios/
├── plugins/            # Installed plugins (generated)
├── hooks/              # Build hooks
└── res/                # Resources (icons, splash screens)
    ├── icon/
    └── screen/

www/index.html

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8">
    <meta name="viewport" content="user-scalable=no, initial-scale=1, maximum-scale=1, minimum-scale=1, width=device-width">
    <title>My App</title>
    <link rel="stylesheet" type="text/css" href="css/index.css">
</head>
<body>
    <div class="app">
        <h1>Apache Cordova</h1>
        <div id="deviceready" class="blink">
            <p class="event listening">Connecting to Device</p>
            <p class="event received">Device is Ready</p>
        </div>
    </div>
    <script type="text/javascript" src="cordova.js"></script>
    <script type="text/javascript" src="js/index.js"></script>
</body>
</html>

www/js/index.js

var app = {
    // Application Constructor
    initialize: function() {
        document.addEventListener('deviceready', this.onDeviceReady.bind(this), false);
    },

    // deviceready Event Handler
    onDeviceReady: function() {
        this.receivedEvent('deviceready');

        // Cordova is now initialized. Have fun!
        console.log('Running cordova-' + cordova.platformId + '@' + cordova.version);

        // Example: Get device information
        console.log('Device Model: ' + device.model);
        console.log('Device Platform: ' + device.platform);
        console.log('Device Version: ' + device.version);
    },

    // Update DOM on a Received Event
    receivedEvent: function(id) {
        var parentElement = document.getElementById(id);
        var listeningElement = parentElement.querySelector('.listening');
        var receivedElement = parentElement.querySelector('.received');

        listeningElement.setAttribute('style', 'display:none;');
        receivedElement.setAttribute('style', 'display:block;');

        console.log('Received Event: ' + id);
    }
};

app.initialize();

CLI Comandos

Project Management

# Create new project
cordova create <path> [id [name [config]]] [options]

# Add platform
cordova platform add <platform-name>
cordova platform add android
cordova platform add ios
cordova platform add browser

# Remove platform
cordova platform remove <platform-name>
cordova platform rm android

# List platforms
cordova platform list
cordova platform ls

# Update platform
cordova platform update <platform-name>

# Check platform version
cordova platform version

Gestión de plugins

# Add plugin
cordova plugin add <plugin-name>
cordova plugin add cordova-plugin-camera
cordova plugin add cordova-plugin-device

# Add plugin with variables
cordova plugin add cordova-plugin-facebook4 --variable APP_ID="123456789" --variable APP_NAME="myApplication"

# Remove plugin
cordova plugin remove <plugin-name>
cordova plugin rm cordova-plugin-camera

# List plugins
cordova plugin list
cordova plugin ls

# Search plugins
cordova plugin search camera

# Update plugin
cordova plugin update <plugin-name>

Construir y correr

# Build for all platforms
cordova build

# Build for specific platform
cordova build android
cordova build ios

# Build with options
cordova build android --release
cordova build ios --device

# Run on emulator
cordova emulate android
cordova emulate ios

# Run on device
cordova run android
cordova run ios

# Run with options
cordova run android --device
cordova run ios --target="iPhone-12"

# Serve for browser testing
cordova serve
cordova serve --port=8080

Comandos de Información

# Check requirements
cordova requirements

# Get help
cordova help
cordova help platform

# Check version
cordova --version
cordova -v

# Get info about project
cordova info

# List available templates
cordova template list

Gestión de la Plataforma

Android Platform

# Add Android platform
cordova platform add android

# Build for Android
cordova build android

# Run on Android emulator
cordova emulate android

# Run on Android device
cordova run android --device

# Build release APK
cordova build android --release

# Build with specific API level
cordova build android --gradleArg=-PcdvBuildToolsVersion=28.0.3

# Clean Android build
cordova clean android

iOS Platform

# Add iOS platform (macOS only)
cordova platform add ios

# Build for iOS
cordova build ios

# Run on iOS simulator
cordova emulate ios

# Run on iOS device
cordova run ios --device

# Build for specific device
cordova build ios --device

# Build with provisioning profile
cordova build ios --codeSignIdentity="iPhone Developer" --provisioningProfile="UUID"

# Clean iOS build
cordova clean ios

Plataforma de navegador

# Add browser platform
cordova platform add browser

# Run in browser
cordova run browser

# Serve for browser testing
cordova serve

# Build for browser
cordova build browser

Sistema Plugin

Plugins de núcleo

# Device Information
cordova plugin add cordova-plugin-device

# Camera
cordova plugin add cordova-plugin-camera

# File System
cordova plugin add cordova-plugin-file

# Network Information
cordova plugin add cordova-plugin-network-information

# Geolocation
cordova plugin add cordova-plugin-geolocation

# Contacts
cordova plugin add cordova-plugin-contacts

# Media
cordova plugin add cordova-plugin-media

# File Transfer
cordova plugin add cordova-plugin-file-transfer

# InAppBrowser
cordova plugin add cordova-plugin-inappbrowser

# Dialogs
cordova plugin add cordova-plugin-dialogs

# Vibration
cordova plugin add cordova-plugin-vibration

# Battery Status
cordova plugin add cordova-plugin-battery-status

# Splash Screen
cordova plugin add cordova-plugin-splashscreen

# Status Bar
cordova plugin add cordova-plugin-statusbar

# Whitelist
cordova plugin add cordova-plugin-whitelist

Ejemplos de uso de plugins

// Device Plugin
document.addEventListener("deviceready", function() {
    console.log("Device Model: " + device.model);
    console.log("Device Platform: " + device.platform);
    console.log("Device Version: " + device.version);
    console.log("Device UUID: " + device.uuid);
    console.log("Device Cordova: " + device.cordova);
}, false);

// Camera Plugin
function takePicture() {
    var options = {
        quality: 75,
        destinationType: Camera.DestinationType.FILE_URI,
        sourceType: Camera.PictureSourceType.CAMERA,
        encodingType: Camera.EncodingType.JPEG,
        targetWidth: 300,
        targetHeight: 300
    };

    navigator.camera.getPicture(onSuccess, onFail, options);

    function onSuccess(imageURI) {
        var image = document.getElementById('myImage');
        image.src = imageURI;
    }

    function onFail(message) {
        alert('Failed because: ' + message);
    }
}

// Geolocation Plugin
function getCurrentPosition() {
    var options = {
        enableHighAccuracy: true,
        timeout: 5000,
        maximumAge: 0
    };

    navigator.geolocation.getCurrentPosition(onSuccess, onError, options);

    function onSuccess(position) {
        console.log('Latitude: ' + position.coords.latitude);
        console.log('Longitude: ' + position.coords.longitude);
        console.log('Altitude: ' + position.coords.altitude);
        console.log('Accuracy: ' + position.coords.accuracy);
        console.log('Timestamp: ' + position.timestamp);
    }

    function onError(error) {
        alert('code: ' + error.code + '\n' + 'message: ' + error.message);
    }
}

// File Plugin
function writeFile() {
    window.requestFileSystem(LocalFileSystem.PERSISTENT, 0, function(fs) {
        fs.root.getFile("test.txt", {create: true, exclusive: false}, function(fileEntry) {
            fileEntry.createWriter(function(fileWriter) {
                fileWriter.onwriteend = function() {
                    console.log("Successful file write...");
                };

                fileWriter.onerror = function(e) {
                    console.log("Failed file write: " + e.toString());
                };

                var dataObj = new Blob(['some file data'], { type: 'text/plain' });
                fileWriter.write(dataObj);
            });
        }, onErrorCreateFile);
    }, onErrorLoadFs);
}

// Network Information Plugin
function checkConnection() {
    var networkState = navigator.connection.type;

    var states = {};
    states[Connection.UNKNOWN]  = 'Unknown connection';
    states[Connection.ETHERNET] = 'Ethernet connection';
    states[Connection.WIFI]     = 'WiFi connection';
    states[Connection.CELL_2G]  = 'Cell 2G connection';
    states[Connection.CELL_3G]  = 'Cell 3G connection';
    states[Connection.CELL_4G]  = 'Cell 4G connection';
    states[Connection.CELL]     = 'Cell generic connection';
    states[Connection.NONE]     = 'No network connection';

    alert('Connection type: ' + states[networkState]);
}

Configuración

config.xml

<?xml version='1.0' encoding='utf-8'?>
<widget id="com.example.myapp" version="1.0.0" xmlns="http://www.w3.org/ns/widgets" xmlns:cdv="http://cordova.apache.org/ns/1.0">
    <name>My App</name>
    <description>
        A sample Apache Cordova application.
    </description>
    <author email="dev@cordova.apache.org" href="http://cordova.io">
        Apache Cordova Team
    </author>

    <!-- Content source -->
    <content src="index.html" />

    <!-- Access control -->
    <access origin="*" />
    <allow-intent href="http://*/*" />
    <allow-intent href="https://*/*" />
    <allow-intent href="tel:*" />
    <allow-intent href="sms:*" />
    <allow-intent href="mailto:*" />
    <allow-intent href="geo:*" />

    <!-- Platform-specific configurations -->
    <platform name="android">
        <allow-intent href="market:*" />
        <icon density="ldpi" src="res/icon/android/ldpi.png" />
        <icon density="mdpi" src="res/icon/android/mdpi.png" />
        <icon density="hdpi" src="res/icon/android/hdpi.png" />
        <icon density="xhdpi" src="res/icon/android/xhdpi.png" />
        <icon density="xxhdpi" src="res/icon/android/xxhdpi.png" />
        <icon density="xxxhdpi" src="res/icon/android/xxxhdpi.png" />
        <splash density="land-ldpi" src="res/screen/android/splash-land-ldpi.png" />
        <splash density="land-mdpi" src="res/screen/android/splash-land-mdpi.png" />
        <splash density="land-hdpi" src="res/screen/android/splash-land-hdpi.png" />
        <splash density="land-xhdpi" src="res/screen/android/splash-land-xhdpi.png" />
        <splash density="land-xxhdpi" src="res/screen/android/splash-land-xxhdpi.png" />
        <splash density="land-xxxhdpi" src="res/screen/android/splash-land-xxxhdpi.png" />
        <splash density="port-ldpi" src="res/screen/android/splash-port-ldpi.png" />
        <splash density="port-mdpi" src="res/screen/android/splash-port-mdpi.png" />
        <splash density="port-hdpi" src="res/screen/android/splash-port-hdpi.png" />
        <splash density="port-xhdpi" src="res/screen/android/splash-port-xhdpi.png" />
        <splash density="port-xxhdpi" src="res/screen/android/splash-port-xxhdpi.png" />
        <splash density="port-xxxhdpi" src="res/screen/android/splash-port-xxxhdpi.png" />
    </platform>

    <platform name="ios">
        <allow-intent href="itms:*" />
        <allow-intent href="itms-apps:*" />
        <icon height="57" src="res/icon/ios/icon.png" width="57" />
        <icon height="114" src="res/icon/ios/icon@2x.png" width="114" />
        <icon height="40" src="res/icon/ios/icon-40.png" width="40" />
        <icon height="80" src="res/icon/ios/icon-40@2x.png" width="80" />
        <icon height="120" src="res/icon/ios/icon-40@3x.png" width="120" />
        <icon height="50" src="res/icon/ios/icon-50.png" width="50" />
        <icon height="100" src="res/icon/ios/icon-50@2x.png" width="100" />
        <icon height="60" src="res/icon/ios/icon-60.png" width="60" />
        <icon height="120" src="res/icon/ios/icon-60@2x.png" width="120" />
        <icon height="180" src="res/icon/ios/icon-60@3x.png" width="180" />
        <icon height="72" src="res/icon/ios/icon-72.png" width="72" />
        <icon height="144" src="res/icon/ios/icon-72@2x.png" width="144" />
        <icon height="76" src="res/icon/ios/icon-76.png" width="76" />
        <icon height="152" src="res/icon/ios/icon-76@2x.png" width="152" />
        <icon height="167" src="res/icon/ios/icon-83.5@2x.png" width="167" />
        <icon height="29" src="res/icon/ios/icon-small.png" width="29" />
        <icon height="58" src="res/icon/ios/icon-small@2x.png" width="58" />
        <icon height="87" src="res/icon/ios/icon-small@3x.png" width="87" />
        <splash height="1136" src="res/screen/ios/Default-568h@2x~iphone.png" width="640" />
        <splash height="1334" src="res/screen/ios/Default-667h.png" width="750" />
        <splash height="2208" src="res/screen/ios/Default-736h.png" width="1242" />
        <splash height="1242" src="res/screen/ios/Default-Landscape-736h.png" width="2208" />
        <splash height="1536" src="res/screen/ios/Default-Landscape@2x~ipad.png" width="2048" />
        <splash height="2048" src="res/screen/ios/Default-Landscape@~ipadpro.png" width="2732" />
        <splash height="768" src="res/screen/ios/Default-Landscape~ipad.png" width="1024" />
        <splash height="2048" src="res/screen/ios/Default-Portrait@2x~ipad.png" width="1536" />
        <splash height="2732" src="res/screen/ios/Default-Portrait@~ipadpro.png" width="2048" />
        <splash height="1024" src="res/screen/ios/Default-Portrait~ipad.png" width="768" />
        <splash height="960" src="res/screen/ios/Default@2x~iphone.png" width="640" />
        <splash height="480" src="res/screen/ios/Default~iphone.png" width="320" />
        <splash height="2732" src="res/screen/ios/Default@2x~universal~anyany.png" width="2732" />
    </platform>

    <!-- Preferences -->
    <preference name="DisallowOverscroll" value="true" />
    <preference name="android-minSdkVersion" value="19" />
    <preference name="BackupWebStorage" value="none" />
    <preference name="SplashMaintainAspectRatio" value="true" />
    <preference name="FadeSplashScreenDuration" value="300" />
    <preference name="SplashShowOnlyFirstTime" value="false" />
    <preference name="SplashScreen" value="screen" />
    <preference name="SplashScreenDelay" value="3000" />

    <!-- Plugin configurations -->
    <plugin name="cordova-plugin-whitelist" spec="1" />
    <plugin name="cordova-plugin-statusbar" spec="2" />
    <plugin name="cordova-plugin-device" spec="2" />
    <plugin name="cordova-plugin-splashscreen" spec="5" />
    <plugin name="cordova-plugin-ionic-webview" spec="^4.0.0" />
    <plugin name="cordova-plugin-ionic-keyboard" spec="^2.0.5" />
</widget>

Plataforma específica Preferencias

<!-- Android-specific preferences -->
<platform name="android">
    <preference name="android-minSdkVersion" value="19" />
    <preference name="android-targetSdkVersion" value="28" />
    <preference name="android-installLocation" value="auto" />
    <preference name="Orientation" value="portrait" />
    <preference name="Fullscreen" value="false" />
    <preference name="KeepRunning" value="true" />
    <preference name="LoadUrlTimeoutValue" value="20000" />
    <preference name="SplashScreen" value="splash" />
    <preference name="SplashScreenDelay" value="3000" />
    <preference name="InAppBrowserStorageEnabled" value="true" />
    <preference name="LoadingDialog" value="My Title,My Message" />
    <preference name="ErrorUrl" value="myErrorPage.html" />
    <preference name="ShowTitle" value="true" />
    <preference name="LogLevel" value="VERBOSE" />
</platform>

<!-- iOS-specific preferences -->
<platform name="ios">
    <preference name="CordovaWebViewEngine" value="CDVUIWebViewEngine" />
    <preference name="MinimumOSVersion" value="10.0" />
    <preference name="AllowInlineMediaPlayback" value="false" />
    <preference name="BackupWebStorage" value="none" />
    <preference name="TopActivityIndicator" value="gray" />
    <preference name="EnableViewportScale" value="false" />
    <preference name="KeyboardDisplayRequiresUserAction" value="true" />
    <preference name="SuppressesIncrementalRendering" value="false" />
    <preference name="SuppressesLongPressGesture" value="false" />
    <preference name="Suppresses3DTouchGesture" value="false" />
    <preference name="GapBetweenPages" value="0" />
    <preference name="PageLength" value="0" />
    <preference name="PaginationBreakingMode" value="page" />
    <preference name="PaginationMode" value="unpaginated" />
</platform>

API de dispositivos

Camera API

// Take picture from camera
function capturePhoto() {
    var options = {
        quality: 50,
        destinationType: Camera.DestinationType.FILE_URI,
        sourceType: Camera.PictureSourceType.CAMERA,
        encodingType: Camera.EncodingType.JPEG,
        targetWidth: 300,
        targetHeight: 300,
        mediaType: Camera.MediaType.PICTURE,
        allowEdit: true,
        correctOrientation: true
    };

    navigator.camera.getPicture(onPhotoSuccess, onPhotoError, options);
}

// Select picture from gallery
function selectPhoto() {
    var options = {
        quality: 50,
        destinationType: Camera.DestinationType.FILE_URI,
        sourceType: Camera.PictureSourceType.PHOTOLIBRARY,
        mediaType: Camera.MediaType.PICTURE,
        allowEdit: true,
        encodingType: Camera.EncodingType.JPEG,
        targetWidth: 300,
        targetHeight: 300
    };

    navigator.camera.getPicture(onPhotoSuccess, onPhotoError, options);
}

function onPhotoSuccess(imageURI) {
    var image = document.getElementById('myImage');
    image.src = imageURI;
}

function onPhotoError(message) {
    alert('Failed because: ' + message);
}

// Cleanup camera resources
function cleanup() {
    navigator.camera.cleanup(onSuccess, onFail);

    function onSuccess() {
        console.log("Camera cleanup success.")
    }

    function onFail(message) {
        alert('Failed because: ' + message);
    }
}

Geolocation API

// Get current position
function getCurrentLocation() {
    var options = {
        enableHighAccuracy: true,
        timeout: 10000,
        maximumAge: 60000
    };

    navigator.geolocation.getCurrentPosition(onLocationSuccess, onLocationError, options);
}

// Watch position changes
var watchID = null;

function watchLocation() {
    var options = {
        enableHighAccuracy: true,
        timeout: 30000,
        maximumAge: 60000
    };

    watchID = navigator.geolocation.watchPosition(onLocationSuccess, onLocationError, options);
}

function stopWatching() {
    if (watchID != null) {
        navigator.geolocation.clearWatch(watchID);
        watchID = null;
    }
}

function onLocationSuccess(position) {
    var element = document.getElementById('geolocation');
    element.innerHTML = 'Latitude: ' + position.coords.latitude + '<br />' +
                        'Longitude: ' + position.coords.longitude + '<br />' +
                        'Altitude: ' + position.coords.altitude + '<br />' +
                        'Accuracy: ' + position.coords.accuracy + '<br />' +
                        'Altitude Accuracy: ' + position.coords.altitudeAccuracy + '<br />' +
                        'Heading: ' + position.coords.heading + '<br />' +
                        'Speed: ' + position.coords.speed + '<br />' +
                        'Timestamp: ' + position.timestamp + '<br />';
}

function onLocationError(error) {
    alert('code: ' + error.code + '\n' + 'message: ' + error.message + '\n');
}

File System API

// Request file system
function requestFileSystem() {
    window.requestFileSystem(LocalFileSystem.PERSISTENT, 0, onFileSystemSuccess, onFileSystemError);
}

function onFileSystemSuccess(fileSystem) {
    console.log(fileSystem.name);
    console.log(fileSystem.root.name);
}

function onFileSystemError(error) {
    console.log(error.code);
}

// Create file
function createFile() {
    window.requestFileSystem(LocalFileSystem.PERSISTENT, 0, function(fs) {
        fs.root.getFile("newFile.txt", {create: true, exclusive: false}, function(fileEntry) {
            console.log("File created: " + fileEntry.fullPath);
        }, onErrorCreateFile);
    }, onErrorLoadFs);
}

// Write to file
function writeToFile() {
    window.requestFileSystem(LocalFileSystem.PERSISTENT, 0, function(fs) {
        fs.root.getFile("newFile.txt", {create: true, exclusive: false}, function(fileEntry) {
            fileEntry.createWriter(function(fileWriter) {
                fileWriter.onwriteend = function() {
                    console.log("Successful file write...");
                    readFile();
                };

                fileWriter.onerror = function(e) {
                    console.log("Failed file write: " + e.toString());
                };

                var dataObj = new Blob(['some file data'], { type: 'text/plain' });
                fileWriter.write(dataObj);
            });
        }, onErrorCreateFile);
    }, onErrorLoadFs);
}

// Read file
function readFile() {
    window.requestFileSystem(LocalFileSystem.PERSISTENT, 0, function(fs) {
        fs.root.getFile("newFile.txt", {}, function(fileEntry) {
            fileEntry.file(function(file) {
                var reader = new FileReader();

                reader.onloadend = function() {
                    console.log("Successful file read: " + this.result);
                    displayFileData(fileEntry.fullPath + ": " + this.result);
                };

                reader.readAsText(file);
            }, onErrorReadFile);
        }, onErrorCreateFile);
    }, onErrorLoadFs);
}

// Delete file
function deleteFile() {
    window.requestFileSystem(LocalFileSystem.PERSISTENT, 0, function(fs) {
        fs.root.getFile("newFile.txt", {create: false}, function(fileEntry) {
            fileEntry.remove(function() {
                console.log("File deleted");
            }, onErrorDeleteFile);
        }, onErrorCreateFile);
    }, onErrorLoadFs);
}

function onErrorLoadFs(error) {
    console.log(error.code);
}

function onErrorCreateFile(error) {
    console.log(error.code);
}

function onErrorReadFile(error) {
    console.log(error.code);
}

function onErrorDeleteFile(error) {
    console.log(error.code);
}

function displayFileData(data) {
    document.getElementById("fileData").innerHTML = data;
}

Contactos API

// Create contact
function createContact() {
    var contact = navigator.contacts.create();
    contact.displayName = "Plumber";
    contact.nickname = "Plumber";

    var name = new ContactName();
    name.givenName = "Jane";
    name.familyName = "Doe";
    contact.name = name;

    var phoneNumbers = [];
    phoneNumbers[0] = new ContactField('work', '212-555-1234', false);
    phoneNumbers[1] = new ContactField('mobile', '917-555-5432', true);
    contact.phoneNumbers = phoneNumbers;

    var emails = [];
    emails[0] = new ContactField('work', 'jane_doe@company.com', false);
    contact.emails = emails;

    contact.save(onContactSaveSuccess, onContactSaveError);
}

function onContactSaveSuccess(contact) {
    alert("Save Success");
}

function onContactSaveError(contactError) {
    alert("Error = " + contactError.code);
}

// Find contacts
function findContacts() {
    var options = new ContactFindOptions();
    options.filter = "Bob";
    options.multiple = true;
    var fields = ["displayName", "name"];
    navigator.contacts.find(fields, onContactFindSuccess, onContactFindError, options);
}

function onContactFindSuccess(contacts) {
    for (var i = 0; i < contacts.length; i++) {
        console.log("Display Name = " + contacts[i].displayName);
    }
}

function onContactFindError(contactError) {
    alert('onError!');
}

// Clone contact
function cloneContact() {
    var clone = contact.clone();
    clone.name.givenName = "John";
    clone.save(onContactSaveSuccess, onContactSaveError);
}

// Remove contact
function removeContact() {
    contact.remove(onContactRemoveSuccess, onContactRemoveError);
}

function onContactRemoveSuccess() {
    alert("Removal Success");
}

function onContactRemoveError(contactError) {
    alert("Error = " + contactError.code);
}

UI Frameworks

jQuery Mobile Integration

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <title>jQuery Mobile App</title>
    <link rel="stylesheet" href="http://code.jquery.com/mobile/1.4.5/jquery.mobile-1.4.5.min.css">
    <script src="http://code.jquery.com/jquery-1.11.3.min.js"></script>
    <script src="http://code.jquery.com/mobile/1.4.5/jquery.mobile-1.4.5.min.js"></script>
</head>
<body>
    <div data-role="page" id="home">
        <div data-role="header">
            <h1>My App</h1>
        </div>
        <div data-role="content">
            <ul data-role="listview" data-inset="true">
                <li><a href="#page2">Page 2</a></li>
                <li><a href="#page3">Page 3</a></li>
            </ul>
            <button onclick="takePicture()">Take Picture</button>
        </div>
        <div data-role="footer">
            <h4>Footer</h4>
        </div>
    </div>

    <div data-role="page" id="page2">
        <div data-role="header">
            <a href="#home" data-icon="arrow-l">Back</a>
            <h1>Page 2</h1>
        </div>
        <div data-role="content">
            <p>This is page 2</p>
        </div>
    </div>

    <script src="cordova.js"></script>
    <script src="js/index.js"></script>
</body>
</html>

Integración Marco7

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, minimum-scale=1, user-scalable=no, minimal-ui, viewport-fit=cover">
    <meta name="apple-mobile-web-app-capable" content="yes">
    <meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
    <title>Framework7 App</title>
    <link rel="stylesheet" href="lib/framework7/css/framework7.bundle.min.css">
    <link rel="stylesheet" href="css/app.css">
</head>
<body>
    <div id="app">
        <div class="view view-main view-init" data-url="/">
            <div class="page" data-name="home">
                <div class="navbar">
                    <div class="navbar-bg"></div>
                    <div class="navbar-inner">
                        <div class="title">My App</div>
                    </div>
                </div>
                <div class="page-content">
                    <div class="block">
                        <p>Welcome to my Cordova app with Framework7!</p>
                        <a class="button button-fill" onclick="takePicture()">Take Picture</a>
                    </div>
                </div>
            </div>
        </div>
    </div>

    <script src="lib/framework7/js/framework7.bundle.min.js"></script>
    <script src="cordova.js"></script>
    <script src="js/app.js"></script>
</body>
</html>

Ionic Framework Integration

# Install Ionic CLI
npm install -g @ionic/cli

# Create Ionic Cordova project
ionic start myApp tabs --type=angular --cordova

# Add platforms
ionic cordova platform add android
ionic cordova platform add ios

# Add plugins
ionic cordova plugin add cordova-plugin-camera

# Build and run
ionic cordova build android
ionic cordova run android

Development Workflow

Recarga en vivo

# Install live reload plugin
cordova plugin add cordova-plugin-browsersync

# Run with live reload
cordova run android --live-reload
cordova run ios --live-reload

# Serve for browser development
cordova serve
cordova serve --port=8080

# Use browser platform for quick testing
cordova platform add browser
cordova run browser

Código caliente

# Install CodePush plugin
cordova plugin add cordova-plugin-code-push

# Configure CodePush
# Add to config.xml:
# <preference name="CodePushDeploymentKey" value="YOUR_DEPLOYMENT_KEY" />

# Release update
code-push release-cordova myApp-Android www 1.0.0
code-push release-cordova myApp-iOS www 1.0.0

Construir ganchos

// hooks/before_build/010_add_platform_class.js
module.exports = function(context) {
    var fs = require('fs');
    var path = require('path');

    var platformsMap = {
        'android': 'android',
        'ios': 'ios',
        'browser': 'browser'
    };

    var platforms = context.opts.cordova.platforms;

    platforms.forEach(function(platform) {
        var indexPath = path.join(context.opts.projectRoot, 'www', 'index.html');
        var indexContent = fs.readFileSync(indexPath, 'utf8');

        // Add platform-specific class to body
        var platformClass = platformsMap[platform];
        if (platformClass) {
            indexContent = indexContent.replace(
                /<body([^>]*)>/,
                '<body$1 class="platform-' + platformClass + '">'
            );

            fs.writeFileSync(indexPath, indexContent);
        }
    });
};

Configuración del medio ambiente

// js/config.js
var Config = {
    development: {
        apiUrl: 'http://localhost:3000/api',
        debug: true
    },
    production: {
        apiUrl: 'https://api.myapp.com',
        debug: false
    }
};

// Detect environment
var environment = 'development';
if (window.location.protocol === 'file:') {
    environment = 'production';
}

var config = Config[environment];

// Usage
console.log('API URL:', config.apiUrl);
console.log('Debug mode:', config.debug);

Construcción y pruebas

Debug Builds

# Build debug version
cordova build android
cordova build ios

# Run debug on device
cordova run android --device --debug
cordova run ios --device --debug

# Enable debugging
cordova build android --debug
cordova build ios --debug

Liberar construye

# Build release version
cordova build android --release
cordova build ios --release

# Sign Android APK
# Generate keystore
keytool -genkey -v -keystore my-release-key.keystore -alias alias_name -keyalg RSA -keysize 2048 -validity 10000

# Sign APK
jarsigner -verbose -sigalg SHA1withRSA -digestalg SHA1 -keystore my-release-key.keystore platforms/android/app/build/outputs/apk/release/app-release-unsigned.apk alias_name

# Align APK
zipalign -v 4 platforms/android/app/build/outputs/apk/release/app-release-unsigned.apk MyApp.apk

Pruebas automatizadas

// test/spec/test.js
describe('App Tests', function() {
    beforeEach(function(done) {
        document.addEventListener('deviceready', done, false);
    });

    it('should have device plugin', function() {
        expect(window.device).toBeDefined();
        expect(device.platform).toBeDefined();
    });

    it('should have camera plugin', function() {
        expect(navigator.camera).toBeDefined();
        expect(navigator.camera.getPicture).toBeDefined();
    });

    it('should take picture', function(done) {
        var options = {
            quality: 50,
            destinationType: Camera.DestinationType.FILE_URI,
            sourceType: Camera.PictureSourceType.CAMERA
        };

        navigator.camera.getPicture(
            function(imageURI) {
                expect(imageURI).toBeDefined();
                done();
            },
            function(error) {
                fail('Camera error: ' + error);
                done();
            },
            options
        );
    });
});

// Run tests with Jasmine
// Include jasmine.js and jasmine-html.js in test.html

Debugging

Depuración remota

# Chrome DevTools for Android
# 1. Enable USB debugging on Android device
# 2. Connect device to computer
# 3. Open Chrome and go to chrome://inspect
# 4. Select your app from the list

# Safari Web Inspector for iOS
# 1. Enable Web Inspector on iOS device (Settings > Safari > Advanced)
# 2. Connect device to Mac
# 3. Open Safari > Develop > [Device Name] > [App Name]

# Weinre (Web Inspector Remote)
npm install -g weinre
weinre --boundHost 0.0.0.0 --httpPort 8080

# Add to index.html:
# <script src="http://YOUR_IP:8080/target/target-script-min.js#anonymous"></script>

Console Logging

// Enhanced console logging
var Logger = {
    log: function(message, data) {
        if (window.console) {
            console.log('[LOG] ' + message, data || '');
        }
        this.writeToFile('LOG', message, data);
    },

    error: function(message, error) {
        if (window.console) {
            console.error('[ERROR] ' + message, error || '');
        }
        this.writeToFile('ERROR', message, error);
    },

    warn: function(message, data) {
        if (window.console) {
            console.warn('[WARN] ' + message, data || '');
        }
        this.writeToFile('WARN', message, data);
    },

    writeToFile: function(level, message, data) {
        // Write logs to file for later analysis
        var logEntry = new Date().toISOString() + ' [' + level + '] ' + message;
        if (data) {
            logEntry += ' ' + JSON.stringify(data);
        }

        // Implementation depends on file plugin
        // this.appendToLogFile(logEntry);
    }
};

// Usage
Logger.log('App started');
Logger.error('Network error', error);
Logger.warn('Low battery', batteryLevel);

Manejo de errores

// Global error handler
window.onerror = function(message, source, lineno, colno, error) {
    Logger.error('Global error: ' + message, {
        source: source,
        line: lineno,
        column: colno,
        error: error
    });

    // Send error to analytics service
    // Analytics.trackError(message, source, lineno);

    return true; // Prevent default browser error handling
};

// Unhandled promise rejection handler
window.addEventListener('unhandledrejection', function(event) {
    Logger.error('Unhandled promise rejection', event.reason);
    event.preventDefault();
});

// Cordova-specific error handling
document.addEventListener('deviceready', function() {
    // Handle plugin errors
    if (window.plugins && window.plugins.toast) {
        window.plugins.toast.showShortTop = function(message) {
            try {
                window.plugins.toast.show(message, 'short', 'top');
            } catch (error) {
                Logger.error('Toast plugin error', error);
                alert(message); // Fallback
            }
        };
    }
}, false);

Optimización del rendimiento

Gestión de memoria

// Efficient DOM manipulation
var DOMUtils = {
    createElement: function(tag, attributes, content) {
        var element = document.createElement(tag);

        if (attributes) {
            for (var attr in attributes) {
                element.setAttribute(attr, attributes[attr]);
            }
        }

        if (content) {
            element.innerHTML = content;
        }

        return element;
    },

    removeElement: function(element) {
        if (element && element.parentNode) {
            element.parentNode.removeChild(element);
        }
    },

    clearElement: function(element) {
        while (element.firstChild) {
            element.removeChild(element.firstChild);
        }
    }
};

// Memory-efficient list rendering
var ListView = {
    render: function(container, items, itemRenderer) {
        // Clear existing content
        DOMUtils.clearElement(container);

        // Use document fragment for efficient DOM updates
        var fragment = document.createDocumentFragment();

        items.forEach(function(item, index) {
            var itemElement = itemRenderer(item, index);
            fragment.appendChild(itemElement);
        });

        container.appendChild(fragment);
    },

    renderVirtual: function(container, items, itemRenderer, visibleCount) {
        // Virtual scrolling for large lists
        var startIndex = 0;
        var endIndex = Math.min(visibleCount, items.length);

        DOMUtils.clearElement(container);

        for (var i = startIndex; i < endIndex; i++) {
            var itemElement = itemRenderer(items[i], i);
            container.appendChild(itemElement);
        }
    }
};

Optimización de imagen

// Lazy loading images
var ImageLoader = {
    loadImage: function(src, callback) {
        var img = new Image();

        img.onload = function() {
            callback(null, img);
        };

        img.onerror = function() {
            callback(new Error('Failed to load image: ' + src));
        };

        img.src = src;
    },

    resizeImage: function(file, maxWidth, maxHeight, quality, callback) {
        var canvas = document.createElement('canvas');
        var ctx = canvas.getContext('2d');
        var img = new Image();

        img.onload = function() {
            var ratio = Math.min(maxWidth / img.width, maxHeight / img.height);
            canvas.width = img.width * ratio;
            canvas.height = img.height * ratio;

            ctx.drawImage(img, 0, 0, canvas.width, canvas.height);

            canvas.toBlob(callback, 'image/jpeg', quality);
        };

        img.src = URL.createObjectURL(file);
    },

    preloadImages: function(urls, callback) {
        var loaded = 0;
        var total = urls.length;
        var images = [];

        if (total === 0) {
            callback(null, images);
            return;
        }

        urls.forEach(function(url, index) {
            this.loadImage(url, function(error, img) {
                if (error) {
                    callback(error);
                    return;
                }

                images[index] = img;
                loaded++;

                if (loaded === total) {
                    callback(null, images);
                }
            });
        }.bind(this));
    }
};

Optimización de la red

// Network request manager
var NetworkManager = {
    cache: {},

    request: function(url, options, callback) {
        options = options || {};

        // Check cache first
        if (options.cache && this.cache[url]) {
            callback(null, this.cache[url]);
            return;
        }

        var xhr = new XMLHttpRequest();

        xhr.onreadystatechange = function() {
            if (xhr.readyState === 4) {
                if (xhr.status >= 200 && xhr.status < 300) {
                    var response = xhr.responseText;

                    try {
                        response = JSON.parse(response);
                    } catch (e) {
                        // Response is not JSON
                    }

                    // Cache successful responses
                    if (options.cache) {
                        this.cache[url] = response;
                    }

                    callback(null, response);
                } else {
                    callback(new Error('HTTP ' + xhr.status + ': ' + xhr.statusText));
                }
            }
        }.bind(this);

        xhr.open(options.method || 'GET', url);

        // Set headers
        if (options.headers) {
            for (var header in options.headers) {
                xhr.setRequestHeader(header, options.headers[header]);
            }
        }

        xhr.send(options.body || null);
    },

    clearCache: function() {
        this.cache = {};
    },

    // Batch requests
    batch: function(requests, callback) {
        var completed = 0;
        var results = [];
        var hasError = false;

        requests.forEach(function(request, index) {
            this.request(request.url, request.options, function(error, response) {
                if (hasError) return;

                if (error) {
                    hasError = true;
                    callback(error);
                    return;
                }

                results[index] = response;
                completed++;

                if (completed === requests.length) {
                    callback(null, results);
                }
            });
        }.bind(this));
    }
};

Seguridad

Política de seguridad del contenido

<!-- Add to index.html -->
<meta http-equiv="Content-Security-Policy" content="
    default-src 'self' data: gap: https://ssl.gstatic.com 'unsafe-eval';
    style-src 'self' 'unsafe-inline';
    media-src *;
    img-src 'self' data: content:;
    connect-src 'self' https://api.myapp.com;
    script-src 'self' 'unsafe-inline' 'unsafe-eval';
">

Almacenamiento seguro

// Secure data storage
var SecureStorage = {
    encrypt: function(data, key) {
        // Use a proper encryption library like CryptoJS
        return CryptoJS.AES.encrypt(JSON.stringify(data), key).toString();
    },

    decrypt: function(encryptedData, key) {
        try {
            var bytes = CryptoJS.AES.decrypt(encryptedData, key);
            var decryptedData = bytes.toString(CryptoJS.enc.Utf8);
            return JSON.parse(decryptedData);
        } catch (error) {
            throw new Error('Failed to decrypt data');
        }
    },

    store: function(key, data, password) {
        var encryptedData = this.encrypt(data, password);
        localStorage.setItem(key, encryptedData);
    },

    retrieve: function(key, password) {
        var encryptedData = localStorage.getItem(key);
        if (!encryptedData) {
            return null;
        }

        return this.decrypt(encryptedData, password);
    },

    remove: function(key) {
        localStorage.removeItem(key);
    }
};

// Usage
SecureStorage.store('userToken', { token: 'abc123' }, 'myPassword');
var userData = SecureStorage.retrieve('userToken', 'myPassword');

Validación de entrada

// Input validation utilities
var Validator = {
    email: function(email) {
        var regex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
        return regex.test(email);
    },

    phone: function(phone) {
        var regex = /^\+?[\d\s\-\(\)]+$/;
        return regex.test(phone) && phone.replace(/\D/g, '').length >= 10;
    },

    url: function(url) {
        try {
            new URL(url);
            return true;
        } catch (error) {
            return false;
        }
    },

    sanitize: function(input) {
        return input.replace(/[<>'"&]/g, function(match) {
            var entities = {
                '<': '&lt;',
                '>': '&gt;',
                '"': '&quot;',
                "'": '&#x27;',
                '&': '&amp;'
            };
            return entities[match];
        });
    },

    length: function(input, min, max) {
        return input.length >= min && input.length <= max;
    },

    required: function(input) {
        return input !== null && input !== undefined && input.toString().trim() !== '';
    }
};

// Form validation
var FormValidator = {
    validate: function(form, rules) {
        var errors = {};

        for (var field in rules) {
            var value = form[field];
            var fieldRules = rules[field];

            for (var rule in fieldRules) {
                var ruleValue = fieldRules[rule];
                var isValid = false;

                switch (rule) {
                    case 'required':
                        isValid = ruleValue ? Validator.required(value) : true;
                        break;
                    case 'email':
                        isValid = ruleValue ? Validator.email(value) : true;
                        break;
                    case 'minLength':
                        isValid = value ? value.length >= ruleValue : true;
                        break;
                    case 'maxLength':
                        isValid = value ? value.length <= ruleValue : true;
                        break;
                }

                if (!isValid) {
                    errors[field] = errors[field] || [];
                    errors[field].push(rule);
                }
            }
        }

        return {
            isValid: Object.keys(errors).length === 0,
            errors: errors
        };
    }
};

// Usage
var formData = {
    email: 'user@example.com',
    password: '123456',
    name: 'John Doe'
};

var rules = {
    email: { required: true, email: true },
    password: { required: true, minLength: 6 },
    name: { required: true, maxLength: 50 }
};

var validation = FormValidator.validate(formData, rules);
if (!validation.isValid) {
    console.log('Validation errors:', validation.errors);
}

Despliegue

App Store Deployment

# iOS App Store
# 1. Build release version
cordova build ios --release

# 2. Open Xcode project
open platforms/ios/MyApp.xcworkspace

# 3. Configure signing and provisioning
# 4. Archive and upload to App Store Connect

# Google Play Store
# 1. Generate signed APK
cordova build android --release

# 2. Sign APK with keystore
jarsigner -verbose -sigalg SHA1withRSA -digestalg SHA1 -keystore my-release-key.keystore platforms/android/app/build/outputs/apk/release/app-release-unsigned.apk alias_name

# 3. Align APK
zipalign -v 4 platforms/android/app/build/outputs/apk/release/app-release-unsigned.apk MyApp.apk

# 4. Upload to Google Play Console
```_

### Distribución de las empresas
```bash
# iOS Enterprise Distribution
# 1. Build with enterprise certificate
cordova build ios --release

# 2. Create IPA file
# 3. Host on internal server with manifest.plist

# Android Enterprise Distribution
# 1. Build signed APK
cordova build android --release

# 2. Distribute through MDM or direct download

Integración continua

# .github/workflows/build.yml
name: Build App

on:
  push:
    branches: [ main ]
  pull_request:
    branches: [ main ]

jobs:
  build:
    runs-on: ubuntu-latest

    steps:
    - uses: actions/checkout@v2

    - name: Setup Node.js
      uses: actions/setup-node@v2
      with:
        node-version: '14'

    - name: Install dependencies
      run: npm install

    - name: Install Cordova
      run: npm install -g cordova

    - name: Add platforms
      run: |
        cordova platform add android
        cordova platform add ios

    - name: Build Android
      run: cordova build android

    - name: Build iOS
      run: cordova build ios

    - name: Run tests
      run: npm test

    - name: Upload artifacts
      uses: actions/upload-artifact@v2
      with:
        name: builds
        path: platforms/*/build/

Buenas prácticas

Project Organization

src/
├── js/
│   ├── app.js              # Main application logic
│   ├── config.js           # Configuration
│   ├── utils.js            # Utility functions
│   ├── services/           # API services
│   ├── models/             # Data models
│   └── views/              # View controllers
├── css/
│   ├── app.css             # Main styles
│   ├── components/         # Component styles
│   └── themes/             # Theme styles
├── templates/              # HTML templates
├── assets/
│   ├── images/
│   ├── fonts/
│   └── sounds/
└── tests/                  # Test files

Código de calidad

// Use strict mode
'use strict';

// Namespace your code
var MyApp = MyApp || {};

MyApp.Utils = {
    // Utility functions
};

MyApp.Services = {
    // Service functions
};

MyApp.Views = {
    // View controllers
};

// Use consistent error handling
MyApp.ErrorHandler = {
    handle: function(error, context) {
        console.error('Error in ' + context + ':', error);

        // Log to analytics service
        if (MyApp.Analytics) {
            MyApp.Analytics.trackError(error, context);
        }

        // Show user-friendly message
        if (navigator.notification) {
            navigator.notification.alert(
                'An error occurred. Please try again.',
                null,
                'Error',
                'OK'
            );
        } else {
            alert('An error occurred. Please try again.');
        }
    }
};

// Use consistent coding style
var MyApp = {
    init: function() {
        this.bindEvents();
        this.setupUI();
    },

    bindEvents: function() {
        document.addEventListener('deviceready', this.onDeviceReady.bind(this), false);
        document.addEventListener('pause', this.onPause.bind(this), false);
        document.addEventListener('resume', this.onResume.bind(this), false);
    },

    onDeviceReady: function() {
        console.log('Device ready');
        this.setupPlugins();
    },

    onPause: function() {
        console.log('App paused');
        this.saveState();
    },

    onResume: function() {
        console.log('App resumed');
        this.restoreState();
    },

    setupPlugins: function() {
        // Initialize plugins
    },

    setupUI: function() {
        // Setup user interface
    },

    saveState: function() {
        // Save application state
    },

    restoreState: function() {
        // Restore application state
    }
};

Directrices de ejecución

```javascript // Optimize for mobile performance var PerformanceOptimizer = { // Debounce function calls debounce: function(func, wait) { var timeout; return function() { var context = this; var args = arguments; clearTimeout(timeout); timeout = setTimeout(function() { func.apply(context, args); }, wait); }; },

// Throttle function calls
throttle: function(func, limit) {
    var inThrottle;
    return function() {
        var args = arguments;
        var context = this;
        if (!inThrottle) {
            func.apply(context, args);
            inThrottle = true;
            setTimeout(function() {
                inThrottle = false;
            }, limit);
        }
    };
},

// Optimize scroll events
optimizeScroll: function(element, callback) {
    var ticking = false;

    function update() {
        callback();
        ticking = false;
    }

    function requestTick() {
        if (!ticking) {
            requestAnimationFrame(update);
            ticking = true;
        }
    }

    element.addEventListener('scroll', requestTick);
},

// Lazy load content
lazyLoad: function(elements, callback) {
    var observer = new IntersectionObserver(function(entries) {
        entries.forEach(function(entry) {
            if (entry.isIntersecting) {
                callback(entry.target);
                observer.unobserve(entry.target);
            }
        });
    });

    elements.forEach(function(element) {
        observer.observe(element);
    });
}

}; ```_

-...

Resumen

Apache Cordova/PhoneGap ofrece una poderosa plataforma para el desarrollo de aplicaciones móviles híbridas:

Negociaciones: - Cross-platform: Escribe una vez, ejecuta en múltiples plataformas - ** Tecnologías web**: Use HTML familiar, CSS y JavaScript - Plugin Ecosystem: Acceda a funciones de dispositivo nativo a través de plugins - Rapid Development: Ciclo de desarrollo más rápido que las aplicaciones nativas - Cost Effective: Base de código única para múltiples plataformas

Mejores casos de uso: - Aplicaciones impulsadas por contenidos - Aplicaciones comerciales - Prototipos y MVP - Aplicaciones con requisitos de interfaz de usuario simples - Aplicaciones multiplataforma con necesidades limitadas de funcionalidad nativa

Consideración: - El rendimiento puede ser más lento que las aplicaciones nativas - Acceso limitado a las últimas características de la plataforma - UI puede no sentirse completamente nativa - Requiere una optimización cuidadosa para un rendimiento suave

Cordova sigue siendo una opción viable para muchos proyectos de aplicaciones móviles, especialmente cuando la velocidad de desarrollo, el costo y la compatibilidad entre plataformas son prioridades sobre el máximo rendimiento y la integración de plataformas nativas.

" copia de la funciónToClipboard() {} comandos const = document.querySelectorAll('code'); que todos losCommands = '; comandos. paraCada(cmd = confianza allCommands += cmd.textContent + '\n'); navigator.clipboard.writeText(allCommands); alerta ('Todos los comandos copiados a portapapeles!'); }

función generaPDF() { ventana.print(); } ■/script título