Estrategias Offline en PWA

  • Por
Explicamos una característica fundamental de las Progressive Web Apps (PWA) como es la capacidad de trabajar offline, junto con las estrategias offline que podemos implementar.

En la actualidad, la mayoría de las Web Apps corren sin capacidades offline, es decir, dependen casi totalmente del servidor, y de que los clientes que se conecten tengan una adecuada conexión a Internet. Sin embargo, gracias a las PWA ya no es un requisito que el usuario disponga de una conexión para poder acceder a una web que consulta habitualmente.

Estando offline habremos observado cientos de veces el juego de dinosaurio (ver imagen más abajo), que es la manera en la que Google Chrome nos dice que no puede mostrar una página por encontrarnos sin conexión. Esto es indicador que esta web, por mucho que presente un diseño responsivo y adaptado a móviles, está dejando pasar la oportunidad de beneficiarse de las características de las web progresivas para mostrar contenido offline, aún estando desconectado de la Red.

Este no es el único escenario que debemos evitar. También es común el escenario de una conexión muy inestable y casi inexistente que llamaremos LiFi la cual no nos muestra ni nuestra App ni el dinosaurio sino que nos deja esperando con la ventana del navegador en blanco. Seguro que habrás percibido también este fatal comportamiento en más de una ocasión.

Existen varias soluciones que ya funcionan bien para manejar estados sin conexión como PouchDB o Firebase, sin embargo, ahora solo con el uso de tecnologías web estándar y abiertas como Service Workers, IndexedDB y el Cache API vamos a poder agregar la capacidad Offline a nuestras Apps.

Así pues, en el marco de nuestro Manual de Progressive Web Apps, vamos a abordar en esta ocasión cómo podemos beneficiarnos de la posiblidad del API de caché para proporcionar a nuestros usuarios una experiencia de uso de las apps, aún cuando se encuentren desconectados de Internet.

Cuándo almacenar la información

Si recuerdas el ciclo de vida de un Service Worker, después de que se registra el Service Worker, empieza a pasar por una serie de eventos que podemos utilizar para realizar nuestra estrategia de Caching

El evento “install” es el mejor momento de realizar nuestro almacenamiento inicial de nuestros archivos, para esto podemos hacer algo así.

sw.js

var CACHENAME = "cachestore";
var FILES = [
  "/index.html",
  "/css/style.css",
  "/js/app.js"
];

self.addEventListener("install", function(event) {
  event.waitUntil(
    caches.open(CACHENAME).then(function(cache) {
      return cache.addAll(FILES);
    })
  );
});
Nota: Hasta el final de este artículo, cada vez que encuentres un código Javascript, ten en cuenta que se trata de un código de implementación de un Service Worker.

Como puedes ver aquí, tenemos nuestros archivos para almacenar en el array FILES, que sería nuestro APP Shell. Entonces entramos en el evento install, el cual trae los elementos del APP Shell desde la red y los almacena en el Cache Storage, dando paso al método activated del Service Worker

Cuándo actualizar nuestros archivos del Cache Storage

Aunque se supone que los archivos que tenemos en nuestro App Shell no van a cambiar frecuentemente, sí lo van a hacer en el futuro. Por ejemplo es habitual que ocurra ante una mejora de UX, un cambio de branding, etc. Es por esto que debemos prepararnos para este momento y no dejar a nuestros usuarios con la versión 1 de nuestro App para siempre.

Para manejar la actualización del Cache Storage, vamos a utilizar un número de versión en el nombre de nuestro Caché, que se definirá en el evento activated del ciclo de vida del Service Worker. Allí podremos realizar la eliminación de los archivos antiguos y cambiarlos por los nuevos.

Actualizando un poco nuestro Service Worker tendriamos algo asi:

var CACHENAME = "cachestore-v1";
var FILES = [
  "/index.html",
  "/css/style.css",
  "/js/app.js"
];

self.addEventListener("install", function(event) {
  event.waitUntil(
    caches.open(CACHENAME).then(function(cache) {
      return cache.addAll(FILES);
    })
  );
}); 

self.addEventListener('activate', function(event) {
  var version = 'v1';
  event.waitUntil(
    caches.keys()
      .then(cacheNames =>
        Promise.all(
          cacheNames
            .map(c => c.split('-'))
            .filter(c => c[0] === 'cachestore')
            .filter(c => c[1] !== version)
            .map(c => caches.delete(c.join('-')))
        )
      )
  );
});

Si observas el código, arriba verás que hemos agregado una serie de operaciones cuando nuestro Service Worker llega al evento ‘activate’. Lo que hacemos es verificar si el nombre que trae en CACHENAME tiene el mismo numero de version que la que tenemos en la variable "version". Si no es así, borramos ese otro cache que hemos detectado. Esto va asegurar que versiones anteriores como cachestore-v0 sean eliminadas del Cache Storage en el cliente.

Ahora ya tienes tus archivos en el Cache Storage y lo puedes actualizar. A continuación vamos a ver como responder a las peticiones de nuestros usuarios.

Cómo responder a Solicitudes (estrategias offline)

Aunque existen diversas mezclas de estrategias para responder a las solicitudes de tus usuarios utilizando el Cache, se han identificado algunos patrones que funcionan bien en la mayoría de los escenarios estos son:

cacheFirst

Este patrón responde a las peticiones con archivos que se encuentran en el Cache Storage. Si falla la respuesta desde el Cache Storage intenta responder con el archivos desde la Red.

self.addEventListener("fetch", function(event) {
  event.respondWith(
    caches.match(event.request).then(function(response) {
      return response || fetch(event.request);
    })
  );
});

cacheOnly

Este patrón responde a todas las peticiones con archivos de Cache Storage. Si no lo encuentra fallará.

Su implementación sería así:

self.addEventListener('fetch', function(event) {
  event.respondWith(caches.match(event.request));
});

networkFirst

Este patrón responde a todas las peticiones con el contenido de la red. Si falla intenta responder con el contenido del Cache Storage.

Su implementación sería así:

self.addEventListener("fetch", function(event) {
  event.respondWith(
    fetch(event.request).catch(function() {
      return caches.match(event.request);
    })
  );
});

networkOnly

Este patrón es el que implementan la mayoría de webs actualmente. Todo es buscado en la red y presentado al usuario. Aunque tal como se ha explicado, hoy es posible mejorar este comportamiento gracias a las aplicaciones progresivas, piensa en esta estrategia cuando se trate de peticiones que no vas a almacenar en el Cache Storage.

Su implementación sería así:

self.addEventListener("fetch", function(event) {
  event.respondWith(
    fetch(event.request)
  );
});

generica

Este patrón responde cuando no se puede obtener un archivo del Cache Storage ni de la red, entonces responde con algo genérico desde el Cache Storage.

Su implementación sería así:

self.addEventListener("fetch", function(event) {
  event.respondWith(
    fetch(event.request).catch(function() {
      return caches.match(event.request).then(function(response) {
        return response || caches.match("/404.html");
      });
    })
  );
});

Conclusión sobre las estrategias offline

Con esta nueva información estamos en capacidad de mejorar nuestro Service Worker, para responder de una manera adecuada en los casos en los que el cliente se encuentre sin conexión a Internet.

Si quieres aprender un poco sobre este asunto te recomendamos visitar el sitio de Mozilla serviceworke.rs el cual contiene una excelente fuente de recetas para nuestros Service Worker.

Autor

Carlos Rojas

Carlos Rojas es speaker en circuitos de tecnología, youtuber y escritor, experto en Progressive Web Apps, Angular, Ionic y Firebase. Google Product Strategy Expert.

Compartir