pwajavascriptworkboxservice workerworkermanifestmanifest.jsonpromesasprimiseprimisesfetchprogresivas
jolugama.com

PWA - Aplicaciones Web Progresivas 1/2

Publicado: por

Tutorial, consejos y primeros pasos para el desarrollo de PWA. Doy ideas muy generales y básicas, que considero esenciales en la realizacion de las mismas. En el siguiente post (2/2) se pasa a la práctica con Workbox.

Aplicación web creado en html, css y javascript principalmente, que se abre en navegador, capaz de trabajar sin conexión, para móvil o escritorio, optimiza la carga de la web con sus distintas estrategias de cacheado. Creado en 2015 por google.

Características

  • Notificaciones push: Es una funcionalidad que hasta hace poco solo tenían las app nativas e híbridas(ionic).
  • Cache (offline): Posibilidad de poder cachear ciertas partes de la web.
  • Multiplataforma: Se abre desde un navegador, tanto móvil como desktop.
  • IndexedDB: Usa la bbdd del propio navegador, haciendo persistente mucha información.
  • Apariencia de app nativa: Ocultándose las barras de herramientas y búsqueda que suele tener los navegadores, junto con un css trabajado aparte, da una apariencia 100% nativa, aunque no está obligado a hacerlo.
  • Acceso directo:Se puede añadir como un icono en el escritorio del pc, o del móvil, haciendo imposible detectar que es una web o PWA, o una app nativa. Para ello hay que pulsar el botón de instalar aplicación o el mismo app mandar popup para ello.
  • HTTPS: Solo funciona en localhost y https, web seguras.
  • Independencia de Markets: No depende de ninguna market, pudiendo estar en un dominio web, con el sistema de posicionamiento que tiene cualquier web, y encontrado desde los buscadores de la misma manera.
  • Manejo de Actualizaciones: El navegador corre en background en el movil, y dispara un evento modal donde indica actualización.

Si quieres saber que navegadores lo soportan: can I use service workers

Si estás en un internet explorer que no sea edge, no va a funcionar, para el resto, muy seguro que si.

Un problema que veo a PWA es que está en continuo cambio, las webs oficiales de google cambian constantemente, siempre están con carteles de que no está actualizado y está pendiente de hacerlo.

Service worker

Pertenece a navigator navigator.serviceWorker, lo tienen incorporado ya todos los navegadores modernos tanto de móvil como escritorio. Es un proxy programable, controlando las solicitudes de red de tu web.

Escuchando eventos y ciclo de vida del service worker

  • El archivo service worker debe estar en la carpeta raiz, para que pueda controlar todos los archivos. Si algún recurso no está en un hijo de donde se aloje sw.js, esté no podrá cachearlos.
  • El evento install es el primero que obtiene un service worker y solo sucede una vez.
  • Una promesa que se pasa a installEvent.waitUntil() señala la duración y el éxito o fracaso de tu instalación.
  • Un service worker no recibirá eventos como fetch y push hasta que se termine de instalar correctamente y su estado sea "activo".
  • De manera predeterminada, los fetch de una página no atravesarán un service worker a menos que la solicitud de la página en sí lo haya hecho. Por lo tanto, tendrás que actualizar la página para ver los efectos del service worker.
  • clients.claim() puede anular esta configuración predeterminada y tomar el control de las páginas no supervisadas.
  • Cuando realizas una llamada a .register(), se descarga el primer service worker. Si tu secuencia de comandos no se descarga, no se analiza o arroja un error en su ejecución inicial, se rechaza la promesa de registro y se descarta el service worker. -self hace referencia al propio service worker.

Register –> install –> (Error or Activated) –> Idle –>(Active or Terminated)

Register

Se indica a serviceWorker donde está el archivo. Solo se ejecutará una vez si el navegador no lo tiene registrado. Si se borra cache, o cambia de navegador, se volverá a ejecutar para proceder a su instalación local.

var url = window.location.href;
// para producción
var swLocation = '/miwpa/sw.js'; 
// si estamos en localhost
if ( navigator.serviceWorker ) {
    if ( url.includes('localhost') ) {
        swLocation = '/sw.js';
    }
}
navigator.serviceWorker.register( swLocation );

Install

El primer evento que recibe un service worker es install. se va a disparar cada la primera vez, y cada vez que haya cambios en ese listener. Pero aún no se activa.

Es el lugar adecuado para almacenar en caché todo lo que necesitas para poder controlar los clientes. La promesa que pasas a event.waitUntil() permite que el navegador sepa que la instalación se completó correctamente.

Si se rechaza la promesa, significa que no se completó la instalación y el navegador elimina el service worker.

// ejemplo de evento install
self.addEventListener('install', e => {
    ...
    e.skipWaiting() // opcional. No recomendable ya que hace saltárselo sin esperas.
    e.waitUntil( Promise.all([ promise1, promise2 ])  ); // opcional
});

Activated

Una vez que tu service worker esté listo para controlar clientes y administrar eventos funcionales como push y sync, recibirás un evento activate. Sin embargo, eso no significa que se controlará la página desde la que se realizó la llamada a .register().

La primera vez que cargas la versión, no se procesa la solicitud. La configuración predeterminada es consistencia: si tu página se carga sin un service worker, tampoco lo harán los subrecursos. Si cargas la versión otra vez (en otras palabras, si actualizas la página), se controlará la solicitud. Tanto la página como la imagen atravesarán eventos fetch.

// ejemplo de evento activate
self.addEventListener('activate', e => {
    const respuesta = caches.keys().then( keys => {
        keys.forEach( key => {
            ...
        });
    });
    e.waitUntil( respuesta ); // opcional
})

Fetch

Todo archivo que se cargue al html pasa por fetch, por lo que se utiliza para aceptar, denegar, cambiar cualquier archivo por otro.

self.addEventListener('fetch', (event) => {
//fetch event handler
});

Fundamentos: promesas

Para usar correctamente los service workers es necesario tener conocimientos de promesas, de fetch (nueva interfaz para hacer peticiones Http) Intentando no alargar este tutorial, existen unas web que lo explican muy bien, en castellano y con ejemplos prácticos.

  • Promesas: crear, unir, manejo de errores, Promise.all y Promise.race Usar_promesas
  • Fetch: Fecth API, fet, post, cabeceras, cors Utilizando_Fetch

Promesas en cadena

Encadenar funciones asíncronas, que esperen unos a otras, transformándolas en síncronas.

function sumarUno(num){
    let promesa = new Promise ((resolve, reject)=> {
    setTimeout ( ()=>{
        resolve(num+1)
    },800);
    })
    return promesa; 
}

sumarUno(5)
.then(sumarUno)  
.then(sumarUno)
.then(sumarUno)
.then(sumarUno)
.then(nuevoNumero=>{
    console.log(nuevoNumero); // 10
})
.catch(error=>{
    console.log('Error en promesa');
    console.log(error);
})

Promise.all

Se ejecutan un array de funciones asyncronas a la vez. Una vez finalizado puede encadenarse otra promesa como resultado.

function sumarLento(numero){
    return new Promise((resolve,reject)=>{
        setTimeout(()=>{
            resolve(numero+1);
        },800)
    })
}

let sumarRapido = (numero)=>{
    return new Promise((resolve,reject)=>{
        setTimeout(()=>{
            resolve(numero+1);
        },300)
    })
}


Promise.all(sumarLento(5),sumarRapido(10))
.then(respuestas=>{
    console.log(respuestas);
})

Promise.race

Solo muestra 1 resultado, el que primero llegue. En este caso será sumarRapido.

Promise.race(sumarLento(5),sumarRapido(10))
.then(respuestas=>{
    console.log(respuestas);
})

Fetch API

Permite recuperar recursos a través de la red. Javascript ha progresado en este sentido, ya que antes era tan complicado que se solía usar siempre jquery. Los frameworks han realizado sus propias versiones, como angularjs y angular 2x con http de httpClient.

Vs antigua de js (no usar)

const request= new XMLHttpRequest();
request.open('GET','https://reqres.in/api/users',true);
request.send(null);
request.onreadystatechange = function(state){
    if(request.readyState===4){
        const resp = JSON.parse(request.response);
        console.log(resp);
    }
}

Vs actual

fetch('https://reqres.in/api/users')
.then(resp => resp.text())
.then(respObj=> {
	console.log(respObj);
    console.log(respObj.page);
    console.log(respObj.per_page);
})

// para reemplazar web por la actual. activar Cors.
fetch('https://wikipedia.org')
.then(resp => resp.text())
.then(html=> {
	document.open();
	document.write(html);
	document.close();
})

Post/Put

let usuario= {
    nombre:'Jose Luis',
    edad: 36
};

fetch('https://reqres.in/api/users',{
    method:'POST',
    body: JSON.stringify(usuario),
    headers: {
        'Content-Type': 'application/json'
    }
})
.then(resp=> resp.json())
.then(console.log)
.catch(error=>{
    console.log('Error en la petición');
    console.log(error);
});

Fetch de Blobs

// enlazo img con la imagen del html
const img = document.querySelector('img');

fetch('mi-imagen.png')
    .then(resp=>resp.blob())
    .then(imagen => {
        let imgPath = URL.createObjectURL(imagen);
        img.src = imagPath;
    })

http-server - servidor web

Se necesita un servidor para arrancar la app. Pueden ser cualquiera, pero este es muy liviano.

www.npmjs.com/package/http-server

Una vez instalado, ejecutar http-server -o Se abrirá una ventana del nevegador.

Debugging

Ya que el responsive, y el móvil es tan importante en las PWA, es necesario que en todo momento trabajemos desde dispositivos móviles o similar.

Se puede hacer de 3 maneras, desde el mismo navegador de pc, no pudiendo probar todo. Otra manera es desde un móvil real, y si no se tiene, virtualizando un dispositivo android.

Desde pc, en chrome - devTools

En herramientas de desarrolladores (f12) - application - service worker:

  • Offline: con ello forzamos a que la web se vea sin conexión. En una web normal se vería el dinosaurio de chrome y nos podriamos jugar unas cuantas partidas, en este caso, al ser cacheado, veremos la web de la misma manera.
  • Update and reload: cuando se cambia algo del service worker, este solo se actualiza en usuario si se cierra la ventana y se abre otra. Se puede forzar por código y también de esta forma. Lo normal es que el usuario no lo tenga activado, actuar en consecuencia.
  • Status: un poco más abajo está status, apunta al service worker actual. Si hemos cambiado el archivo y no está activado el anterior punto, aparecerá waiting to active <skipWaiting>. Si pulsamos en esa url se actualizará el service Worker, igual que si pulsaramos Update and reload.
  • update / unregister: si pulsamos en unregister, borramos todo el sw, y en la próxima carga, lanzará todos los eventos desde el principio. Ojo, esto no limpia la cache, se deberá también en ese caso limpiarlo. (clear storage)

En herramientas de desarrolladores (f12) - application - cache: Cache storage: es donde se guardan nuestros archivos cacheados. De esta forma cuando la web no tenga conexión, lo cargará del cache.

En herramientas de desarrolladores (f12) - application - clear storage: clear site data: es el método más eficaz para dejar localhost o la url donde estás como limpio, al volver a cargar todo empieza de nuevo, de 0.

Desde dispositivo real

  • Para ello debemos de tener habilitadas las opciones de desarrollador, que por defecto están ocultas. Depende de la versión de android, y de la capa de más que tenga, por ejemplo xiaomi dispone de su propia forma de mostrarla. La mayoría están en ajustes - sobre el teléfono - versión (o build). Se pulsa 8 veces.

  • En ajustes adicionales - Opciones de desarrollador (puede tener otro nombre), habilitamos el primer check, y depuración usb.
  • click en chrome, en devTools - los 3 puntitos - more tools - remote devices
  • En settings habilitamos los 2 check. añadimos nueva regla: 8080 y localhost:8080 (o el puerto que sea)
  • click en nuestro dispositivo (previamente nuestro android nos pide que aceptemos): abrimos localhost:8080. (antes debemos abrir chrome en android).
  • click en Inspect. En la pestaña application disponemos de todo lo necesario para borrar, ver todo sobre el service worker de nuestro móvil.

Desde dispositivo emulado

  • Lo primero de todo es instalar android studio: https://developer.android.com/studio
  • Abrimos android studio, start a new android studio project, ponemos cualquier nombre, next, seleccionamos un api actual, superior de la 4.4, por ejemplo la 26, next, empty activity, next, finish.
  • Click en el icono play verde (run). Creamos un nuevo virtual device, en mi caso el pixel 2 xl. Una vez descargado, ejecutamos dicho dispositivo virtual.
  • En remote devices de devTools de chrome, si tenemos habilitado todo del anterior punto, podemos ver el dispositivo detectado, al igual que igualmente podemos inspeccionarlo, como un dispositivo más. Es muy útil para probar resoluciones como tablets o móviles pequeños.

Se abre el dispositivo emulado en pantalla. En ese dispositivo abre chrome escribe: localhost:8080.

Desde el pc, podemos hacer igual que el punto anterior: chrome://inspect/#devices.

Lighthouse - mejora de calidad web

En chrome, f12, Lighthouse (antes llamado audit) disponemos de una herramienta para ver la calidad de cualquier web. Está separado en bloques, uno de ellos es service Worker. Si no tiene aparecerá en gris.

Esta herramienta es útil para poder mejorar la calidad de nuestras webs. Cualquier cambio que hagas en tu localhost puedes volver a pulsar y ver si se han solucionado los errores marcados.

Los bloques son:

  • Performance (Rendimiento)
  • Accesibility (Accesibilidad)
  • Best practices (Mejores prácticas)
  • SEO
  • Progressive Web App

Puedo decir que llegar a más de un 80 según en que sección es todo un logro. Ni youtube lo consigue, que es una WPA. Simplemente, intentar conseguir el mayor número verifando los errores, los cuales son muy claros.

Manifest.json

Archivo JSON que proporciona los metadatos necesarios para que la PWA pueda comportarse de manera más similar a una aplicación nativa: se puede instalar en la pantalla de inicio y es capaz de realizar transiciones suaves en la pantalla de inicio.

Configurar el json significa describir cómo se verá su PWA en la pantalla de inicio del usuario, así como cómo se verá cuando el usuario inicie su aplicación por primera vez. Además, el comportamiento de la interfaz de usuario del navegador (si estará visible u oculta).

Compatible con Chrome, Edge, el navegador de Android, Chrome para Android, Firefox para Android y Samsung Internet. Es parcialmente compatible con Safari.

Se puede crear manualmente, aunque hay webs que nos facilitan el trabajo. Un ejemplo es: app-manifest.firebaseapp.com Y no solo eso, en dicha web, podemos añadir una imagen 512x512 y nos genera todas los tamaños necesarios, todo en un zip.

Si quieres saber más sobre el archivo manifest, consulta la siguente web: developer.mozilla.org/es/docs/Web/Manifest

En la raiz, creamos nuestro archivo manifest.json

{
  "name": "PWA Misiones espaciales",
  "short_name": "PWA espacial",
  "lang": "es-ES",
  "start_url": "/index.html",
  "display": "standalone",
  "theme_color": "#ff00ed",
  "background_color": "#ff8200",
  "icons": [
    {
      "src": "images/touch/icon-128x128.png",
      "sizes": "128x128"
    },
    {
      "src": "images/touch/icon-192x192.png",
      "sizes": "192x192"
    },
    {
      "src": "images/touch/icon-256x256.png",
      "sizes": "256x256"
    },
    {
      "src": "images/touch/icon-384x384.png",
      "sizes": "384x384"
    },
    {
      "src": "images/touch/icon-512x512.png",
      "sizes": "512x512"
    }
  ]
}

En index.html, tag head, añadimos este código, para referenciar manifest y configurar algunos metas necesarios (algunos navegadores no soportan todas las características de manifest)

<link rel="manifest" href="manifest.json">

<!-- Todo lo demás no es obligatorio -->
<meta name="mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="application-name" content="PWA Misiones espaciales">
<meta name="apple-mobile-web-app-title" content="PWA Misiones espaciales">
<meta name="theme-color" content="#ff00ed">
<meta name="msapplication-navbutton-color" content="#ff00ed">
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
<meta name="msapplication-starturl" content="/index.html">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">

<link rel="icon" sizes="128x128" href="/images/touch/icon-128x128.png">
<link rel="apple-touch-icon" sizes="128x128" href="/images/touch/icon-128x128.png">
<link rel="icon" sizes="192x192" href="icon-192x192.png">
<link rel="apple-touch-icon" sizes="192x192" href="/images/touch/icon-192x192.png">
<link rel="icon" sizes="256x256" href="/images/touch/icon-256x256.png">
<link rel="apple-touch-icon" sizes="256x256" href="/images/touch/icon-256x256.png">
<link rel="icon" sizes="384x384" href="/images/touch/icon-384x384.png">
<link rel="apple-touch-icon" sizes="384x384" href="/images/touch/icon-384x384.png">
<link rel="icon" sizes="512x512" href="/images/touch/icon-512x512.png">
<link rel="apple-touch-icon" sizes="512x512" href="/images/touch/icon-512x512.png">

Comprobamos si tenemos bien configurado nuestro archivo manifest.json:

  • chrome - devTools(f12) - application - clear storage - clear site data
  • chrome - ctrl + f5 (actualizado)
  • chrome - devTools - application - manifest

Ya podemos ver, si está todo bien, de forma más visual nuestro archivo manifest, con sus colores, iconos, y resto de configuración.

Volvemos a probar Audit (chrome - devTools - audit). Podemos comprobar que tenemos nuestra PWA funcionando perfectamente.

Como podemos comprobar, uno de los fallos es 'Does not redirect HTTP traffic to HTTPS'. Es decir, nos indica que al ejecutarse en localhost y no en una url con https, no vamos a poder disfrutar de todo el potencial de un PWA.

Notificaciones push

Permite enviar mensajes desde un servidor a un navegador (no tiene porqué estar abierto). Se contempla en 2 fases

  • push: el envío desde el servidor, hacia todos los subscriptores.
  • notificación: el mensaje en sí. la información, enforma de modal.

Comprobar compatibilidad navegador

if ('Notification' in window && navigator.serviceWorker) {
  // Mostrar el UI para dejar que el usuario acepte poder recibir notificaciones.
}

Queckear permiso


if (Notification.permission === "granted") {
  /* Aquí es donde haces tu magia ;) */
} else if (Notification.permission === "blocked") {
 /* El usuario ha negado previamente hacer notificaciones. No se puede hacer nada */
} else {
  /* Preguntar si da permiso el usuario */
}

Solicitar permiso

Antes de poder enviar cualquier notificación push hay que solicitar permiso. Esto no es configurable y debe ser enmascarado con un modal previo customizado para solicitarlo en un momento, desaconsejando al principio de la carga como la gran mayoría hace.

Notification.requestPermission(function(status) {
    console.log('Notification permission status:', status);
});

Notificación push custom desde local

Aunque esto no es lo habitual, ya que suele ser de cara al servidor, y no el propio cliente quien genere la notificación.

Tiene un título, un cuerpo y un icono. Se le puede añadir vibración (si usas móvil funcionará), así como la fecha en la que se va a recibir.

function displayNotification() {
  if (Notification.permission == 'granted') {
    navigator.serviceWorker.getRegistration().then(function(reg) {
      var options = {
        body: 'Aquí el cuerpo del mensaje',
        icon: 'images/icons/icon-512x512.png',
        vibrate: [100, 50, 100],
        data: {
          dateOfArrival: Date.now(),
          primaryKey: 1
        }
      };
      reg.showNotification('Esto es un título', options);
    });
  }
}

// hace que se muestre la notificación.
displayNotification()

Notificaciones Push desde Angular

Para ello hay que seguir unos pasos muy bien explicados en esta web. angular-university.io/angular-push-notifications

Transformación de web a PWA

Manualmente

Desaconsejado, son muchos pasos que de otra manera te los puedes saltar.

  • Añadir un archivo js. En él se deben de crear todos los eventos de serviceWorker, pasando por todas las etapas, definiendo los tipos de cacheados. Es muy fácil equivocarse y el tiempo se hace largo.
  • Añadir manifest.json.

Workbox

Workbox es una colección de distintas librerías y herramientas creadas por Google y que nos ayudan en la creación y simplificación de service workers para nuestras Progressive Web Apps.

Ver post 2/2 de PWA.

Angular e Ionic

Para más información en la web de ionic ionicframework.com/docs/angular/pwa

ng add @angular/pwa
 
... y finalmente... Si te ha gustado, difúndelo. Es solo un momento. escríbe algo en comentarios 😉 Gracias.