Progressive Web
Applications
(PWA)
Concepto y estructura básica
App manifest.
Service Workers.
Estrategias de caching con Service Workers.
Web Push Notifications.
PWA
Diferencias entre aplicaciones nativas y webs
Ventajas de web y de apps nativas
Aplicaciones progresivas y multiplataforma
Aplicaciones web (HTML, CSS, JS)
Responsive
Rápidas
Trabajan offline
Un solo lenguaje
Fácil distribución
Enganchan al usuario
Aparecen en Google
Seguras
Entorno de desarrollo
Editor: Visual Studio Code
Configurar:
Format on Paste y Format on Save
Node.js y npm
Extensión ColorZilla para el navegador
Android Studio para el emulador de Android
Xcode para el emulador de iOS
Comandos básicos
Clonar un repositorio:
git clone URL
Descargar última versión del repositorio:
git pull origin master
Configuración proxy
git config --global http.proxy http://username:password@host:port
git config --global https.proxy http://username:password@host:port
npm
Instalar última versión después de instalar Node.js
(configurar proxy si es necesario): npm install -g npm
Repositorio de módulos distribuibles
Módulos globales y módulos locales
La carpeta node_modules
El archivo package.json:
Registro de dependencias
Dependencias de desarrollo y de producción
Versiones (SEMVER)
Comandos npm
Instalar un paquete globalmente:
npm install -g paquete
Instalar un paquete de producción:
npm install paquete
Instalar un paquete de desarrollo:
npm install paquete --save-dev
Instalar todas las dependencias:
npm install
Instalar las dependencias de producción:
npm install --production
Listar paquetes instalados:
npm list --depth=0 (locales)Comandos npm
Lanzar el ejecutable de un paquete:
npx ejecutable
(aunque no esté instalado)
Configuración proxy
npm config set proxy http://username:password@host:port
npm config set https-proxy http://username:password@host:port
Funciones
Pasar funciones anónimas como parámetros
Funciones callback
Funciones arrow
map() y filter()
Promesas
Procesos asíncronos
Dos métodos principales: then() y catch()
Encadenado de promesas
El método finally()
Promise.all()
async / await
Fetch API
Request y Response
La función fetch()
Nos interesa del Request:
method
mode
destination
headers
url
body
Fetch API
Nos interesa del Response:
ok
status
headers
body
body.text()
body.json()
Lighthouse
Integrada en Chrome
También disponible como paquete npm
Nos guía para saber qué le falta a nuestra PWA
Web manifest
Información sobre la apariencia de la app instalada
Archivo JSON
<link rel="manifest">
Web manifest
Estructura:
{
"name": "Nombre de la app",
"short_name": "Nombre corto",
"theme_color": "#2196f3",
"background_color": "#2196f3",
"display": "standalone",
"start_url": "/",
"icons": [
{
"src": "images/icons/icon-192x192.png",
"sizes": "192x192",
"type": "image/png"
},
{
"src": "images/icons/icon-512x512.png",
"sizes": "512x512",
"type": "image/png"
}
]
}
Web manifest
Iconos para web
<link rel="icon" type="image/png" sizes="32x32" href="<ruta>/favicon-32x32.png">
<link rel="icon" type="image/png" sizes="16x16" href="<ruta>/favicon-16x16.png">
<link rel="shortcut icon" href="<ruta>/favicon.ico">
Web manifest
Iconos para Android (mínimo 192x192 y 512x512)
"icons": [
{
"src": "images/icons/icon-72x72.png",
"sizes": "72x72",
"type": "image/png"
},
{
"src": "images/icons/icon-96x96.png",
"sizes": "96x96",
"type": "image/png"
},
{
"src": "images/icons/icon-128x128.png",
"sizes": "128x128",
"type": "image/png"
},
{
"src": "images/icons/icon-144x144.png",
"sizes": "144x144",
"type": "image/png"
},
{
"src": "images/icons/icon-152x152.png",
"sizes": "152x152",
"type": "image/png"
},
{
"src": "images/icons/icon-192x192.png",
"sizes": "192x192",
"type": "image/png"
},
{
"src": "images/icons/icon-384x384.png",
"sizes": "384x384",
"type": "image/png"
},
{
"src": "images/icons/icon-512x512.png",
"sizes": "512x512",
"type": "image/png"
}
]
Web manifest
iOS 🤯 (documentación)
<meta name="viewport" content="initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0, user-scalable=no, viewport-fit=cover">
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-touch-fullscreen" content="yes">
<meta name="apple-mobile-web-app-title" content="appTitle">
<meta name="apple-mobile-web-app-status-bar-style" content="default">
<link rel="apple-touch-icon" sizes="180x180" href="<ruta>/apple-touch-icon.png">
<link rel="apple-touch-startup-image" media="(device-width: 375px) and (device-height: 812px) and (-webkit-device-pixel-ratio: 3)" href="<ruta>/apple-launch-1125x2436.png">
<link rel="apple-touch-startup-image" media="(device-width: 375px) and (device-height: 667px) and (-webkit-device-pixel-ratio: 2)" href="<ruta>/apple-launch-750x1334.png">
<link rel="apple-touch-startup-image" media="(device-width: 414px) and (device-height: 736px) and (-webkit-device-pixel-ratio: 3)" href="<ruta>/apple-launch-1242x2208.png">
<link rel="apple-touch-startup-image" media="(device-width: 320px) and (device-height: 568px) and (-webkit-device-pixel-ratio: 2)" href="<ruta>/apple-launch-640x1136.png">
<link rel="apple-touch-startup-image" media="(device-width: 768px) and (device-height: 1024px) and (-webkit-device-pixel-ratio: 2)" href="<ruta>/apple-launch-1536x2048.png">
<link rel="apple-touch-startup-image" media="(device-width: 834px) and (device-height: 1112px) and (-webkit-device-pixel-ratio: 2)" href="<ruta>/apple-launch-1668x2224.png">
<link rel="apple-touch-startup-image" media="(device-width: 1024px) and (device-height: 1366px) and (-webkit-device-pixel-ratio: 2)" href="<ruta>/apple-launch-2048x2732.png">
Service workers
JavaScript que corre en un hilo paralelo
No tienen acceso al DOM ni al objeto window
Se comunican con el hilo principal mediante postMessage()
Proxy entre la app y la red
Registro de un SW
Ciclo de vida de un SW
Service workers
Condiciones para que la PWA sea instalable:
Manifest con:
name o short_name
start_url
display: "standalone" | "fullscreen" | "minimal-ui"
icons: 512 y 192
Service Worker con evento fetch
HTTPS
Service workers
El prompt de instalación
El evento beforeinstallprompt:
preventDefault() para que no aparezca el de Android
Lanzar el prompt con un gesto del usuario
Leer userChoice
Comprobar si se está abriendo la app instalada
let eventoInstall;
window.addEventListener('beforeinstallprompt', e => {
eventoInstall = e;
e.preventDefault();
document.querySelector('.install').removeAttribute('hidden');
})
document.querySelector('.install').addEventListener('click', () => {
eventoInstall.prompt();
eventoInstall.userChoice.then(ch => {
if (ch.outcome === 'accepted') {
console.log('El usuario ha aceptado instalar');
} else {
console.log('El usuario no ha aceptado instalar');
}
document.querySelector('.install').setAttribute('hidden', true);
});
});
window.addEventListener('appinstalled', () => console.log("El usuario ha instalado la aplicación"));
if (window.matchMedia('(display-mode: standalone)').matches) {
console.log('Desde la app instalada');
}
Caché
CacheStorage API:
open()
addAll()
put()
match()
keys()
Estrategias de caching
Cache only
Estrategias de caching
Cache only
Network only
Estrategias de caching
Cache only
Network only
Cache with fallback to network
Estrategias de caching
Cache only
Network only
Cache with fallback to network
Network with fallback to cache
Estrategias de caching
Cache only
Network only
Cache with fallback to network
Network with fallback to cache
Stale while revalidate
Estrategias de caching
Cache only
Network only
Cache with fallback to network
Network with fallback to cache
Stale while revalidate
Generic fallback
Estrategias de caching
Cache only
Network only
Cache with fallback to network
Network with fallback to cache
Stale while revalidate
Generic fallback
Cache then network
Workbox
Instalar:
npm install [-g] [--save-dev] workbox-cli
Generar configuración:
workbox wizard --injectManifest
El SW fuente tiene que tener esta línea:
workbox.precaching.precacheAndRoute([]);
workbox injectManifest
importScripts('https://storage.googleapis.com/workbox-cdn/releases/4.3.1/workbox-sw.js');
workbox.routing.registerRoute(
/\.css$/,
new workbox.strategies.CacheFirst()
);
workbox.routing.registerRoute(
/\.(?:png|gif|jpg|jpeg|svg)$/,
new workbox.strategies.StaleWhileRevalidate({
cacheName: '',
plugins: [
new workbox.expiration.Plugin({
maxEntries: 100,
maxAgeSeconds: 7 * 24 * 60 * 60,
}),
],
})
);
Notificaciones PUSH
Entran en juego:
La app
El Service Worker
El servicio PUSH del navegador
Un servicio de Google, Firefox...
Un servidor PUSH
Notificaciones PUSH
La app:
Puede lanzar el prompt para preguntar al usuario si da permisos para notificaciones:
Notification.requestPermission()
Puede preguntar si el usuario ya ha dado permisos o no:
Notification.permission
('default', 'granted' o 'denied')
Puede pedir una suscripción al servicio PUSH:
reg.pushManager.subscribe({
userVisibleOnly: true,
applicationServerKey: <llave pública VAPID>
});
Notificaciones PUSH
La app:
Cuando obtiene una suscripción del servicio PUSH, la envía a nuestro servidor PUSH, que la almacenará.
Notificaciones PUSH
El servidor PUSH:
Genera las claves VAPID
Utiliza la librería web-push para enviar notificaciones PUSH.
Notificaciones PUSH
El Service Worker:
Escucha el evento 'push'
Emite una notificación al SO:
self.registration.showNotification(título, opciones)
Puede capturar el click en la notificación escuchando al evento 'notificationclick'
Links