Progressive Web
Applications

(PWA)

Licencia de Creative Commons mario@mariogl.com

Temario

  • Concepto y estructura básica

  • App manifest.

  • Service Workers.

  • Estrategias de caching con Service Workers.

  • Web Push Notifications.

Introducción a las PWA

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

Entorno de desarrollo

Git

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

Node.js y npm

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)
        npm list -g --depth=0   (globales)

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

JavaScript

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()

PWA

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

<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

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