La vida de un service worker

Es difícil saber qué hacen los service workers sin comprender su ciclo de vida. Su funcionamiento interno parecerá opaco, incluso arbitrario. Resulta útil recordar que, como cualquier otra API de navegador, los comportamientos de los service worker están bien definidos y especificados, y posibilitan las aplicaciones sin conexión, a la vez que se facilitan las actualizaciones sin interrumpir la experiencia del usuario.

Antes de hablar de Workbox, es importante comprender el ciclo de vida del service worker para que la función tenga sentido.

Definición de términos

Antes de entrar en el ciclo de vida del service worker, vale la pena definir algunos términos sobre cómo funciona ese ciclo de vida.

Control y alcance

La idea de control es crucial para comprender cómo funcionan los service workers. Una página que se describe como controlada por un service worker es una página que permite que un service worker intercepte solicitudes de red en su nombre. El service worker está presente y puede realizar el trabajo de la página dentro de un alcance determinado.

Permiso

El alcance de un service worker se determina según su ubicación en un servidor web. Si un service worker se ejecuta en una página ubicada en /subdir/index.html y en /subdir/sw.js, su alcance es /subdir/. Para ver el concepto de alcance en acción, consulta este ejemplo:

  1. Navega a https://service-worker-scope-viewer.glitch.me/subdir/index.html. Aparecerá un mensaje que indica que ningún service worker está controlando la página. Sin embargo, esa página registra un service worker de https://service-worker-scope-viewer.glitch.me/subdir/sw.js.
  2. Vuelve a cargar la página. Debido a que el service worker se registró y ahora está activo, controla la página. Será visible un formulario que contiene el alcance, el estado actual y su URL del service worker. Nota: Ten en cuenta que tener que volver a cargar la página no tiene nada que ver con el alcance, sino con el ciclo de vida del service worker, que se explicará más adelante.
  3. Ahora, navega a https://service-worker-scope-viewer.glitch.me/index.html. Aunque se registró un service worker en este origen, todavía hay un mensaje que indica que no existe un service worker actual. Esto se debe a que esta página no está dentro del alcance del service worker registrado.

El alcance limita las páginas que controla el service worker. En este ejemplo, eso significa que el service worker cargado desde /subdir/sw.js solo puede controlar páginas ubicadas en /subdir/ o su subárbol.

Lo anterior es cómo funciona el alcance de forma predeterminada, pero el alcance máximo permitido se puede anular si se configura el encabezado de respuesta Service-Worker-Allowed y se pasa una opción scope al método register.

A menos que exista un buen motivo para limitar el alcance del service worker a un subconjunto de un origen, carga un service worker desde el directorio raíz del servidor web para que su alcance sea lo más amplio posible, y no te preocupes por el encabezado Service-Worker-Allowed. De esa forma, es mucho más fácil para todos.

Cliente

Cuando se dice que un service worker controla una página, en realidad controla un cliente. Un cliente es cualquier página abierta cuya URL se encuentre dentro del alcance de ese service worker. Específicamente, estas son instancias de un WindowClient.

Ciclo de vida de un nuevo service worker

Para que un service worker controle una página, primero debe existir, por así decirlo. Comencemos con lo que sucede cuando se implementa un service worker nuevo en un sitio web sin un service worker activo.

Registro

El registro es el paso inicial del ciclo de vida del service worker:

<!-- In index.html, for example: -->
<script>
  // Don't register the service worker
  // until the page has fully loaded
  window.addEventListener('load', () => {
    // Is service worker available?
    if ('serviceWorker' in navigator) {
      navigator.serviceWorker.register('/sw.js').then(() => {
        console.log('Service worker registered!');
      }).catch((error) => {
        console.warn('Error registering service worker:');
        console.warn(error);
      });
    }
  });
</script>

Este código se ejecuta en el subproceso principal y hace lo siguiente:

  1. Debido a que la primera visita del usuario a un sitio web ocurre sin un service worker registrado, debes esperar a que la página esté completamente cargada antes de registrar uno. Esto evita la contención del ancho de banda si el service worker almacena previamente en caché algo.
  2. Si bien el service worker es compatible, realizar una verificación rápida ayuda a evitar errores en los navegadores que no lo son.
  3. Cuando la página esté completamente cargada y si se admite el service worker, registra /sw.js.

Estos son algunos aspectos clave que debes comprender:

  • Los service workers solo están disponibles a través de HTTPS o localhost.
  • Si el contenido de un service worker contiene errores de sintaxis, el registro falla y el service worker se descarta.
  • Recordatorio: Los service workers operan dentro de un alcance. Aquí, el alcance es el origen completo, como se cargó desde el directorio raíz.
  • Cuando comienza el registro, el estado del service worker se establece en 'installing'.

Cuando finalice el registro, comenzará la instalación.

Instalación

Un service worker activa su evento install después del registro. Solo se llama una vez a install por service worker y no se volverá a activar hasta que se actualice. Se puede registrar una devolución de llamada para el evento install en el alcance del trabajador con addEventListener:

// /sw.js
self.addEventListener('install', (event) => {
  const cacheKey = 'MyFancyCacheName_v1';

  event.waitUntil(caches.open(cacheKey).then((cache) => {
    // Add all the assets in the array to the 'MyFancyCacheName_v1'
    // `Cache` instance for later use.
    return cache.addAll([
      '/css/global.bc7b80b7.css',
      '/css/home.fe5d0b23.css',
      '/js/home.d3cc4ba4.js',
      '/js/jquery.43ca4933.js'
    ]);
  }));
});

Esto crea una instancia nueva de Cache y almacena previamente los elementos en caché. Tendremos muchas oportunidades para hablar sobre el almacenamiento previo en caché más adelante, así que nos enfocaremos en la función de event.waitUntil. event.waitUntil acepta una promesa y espera hasta que se resuelva. En este ejemplo, esa promesa realiza dos acciones asíncronas:

  1. Crea una nueva instancia de Cache llamada 'MyFancyCache_v1'.
  2. Después de crear la caché, se almacena previamente en caché un arreglo de URLs de elementos con su método addAll asíncrono.

La instalación falla si se rechazan las promesas que se pasaron a event.waitUntil. Si esto sucede, se descarta el service worker.

Si la promesa resolve, la instalación se realiza correctamente, y el estado del service worker cambia a 'installed' y, luego, se activa.

Activación

Si el registro y la instalación se realizan correctamente, el service worker se activa, y su estado se convierte en 'activating'. El trabajo se puede realizar durante la activación en el evento activate del service worker. En este evento, una tarea típica es reducir el almacenamiento en caché anterior. Sin embargo, en el caso de un service worker nuevo, esto no es relevante por el momento y se ampliará cuando hablemos de las actualizaciones de service worker.

En el caso de los service workers nuevos, activate se activa de inmediato después de que install tiene éxito. Una vez finalizada la activación, el estado del service worker pasa a ser 'activated'. Ten en cuenta que, de forma predeterminada, el nuevo service worker no comenzará a controlar la página hasta la siguiente navegación o actualización de la página.

Cómo administrar actualizaciones de service workers

Una vez que se implementa el primer service worker, es probable que debas actualizarlo más adelante. Por ejemplo, es posible que se requiera una actualización si se producen cambios en el manejo de la solicitud o en la lógica de almacenamiento previo en caché.

Cuándo se realizan las actualizaciones

Los navegadores buscarán actualizaciones de un service worker en los siguientes casos:

  • El usuario navega a una página dentro del alcance del service worker.
  • Se llama a navigator.serviceWorker.register() con una URL diferente de la del service worker actualmente instalado, pero no la cambies.
  • Se llama a navigator.serviceWorker.register() con la misma URL que el service worker instalado, pero con un alcance diferente. Una vez más, si es posible, evita esto manteniendo el alcance en la raíz del origen.
  • Cuando se hayan activado eventos como 'push' o 'sync' en las últimas 24 horas, pero no te preocupes por ellos aún.

Cómo se realizan las actualizaciones

Es importante saber cuándo el navegador actualiza un service worker, pero también lo es el "cómo". Si suponemos que la URL o el alcance de un service worker no se modificarán, un service worker actualmente instalado solo se actualizará a una versión nueva si su contenido cambió.

Los navegadores detectan los cambios de dos maneras:

  • Cualquier cambio byte por byte en las secuencias de comandos que solicite importScripts, si corresponde
  • Cualquier cambio en el código de nivel superior del service worker, que afecte la huella digital que el navegador generó del trabajador

El navegador hace mucho trabajo pesado aquí. A fin de asegurarte de que el navegador tenga todo lo que necesita para detectar de manera confiable los cambios en el contenido de un service worker, no le digas a la caché HTTP que la conserve ni cambies su nombre de archivo. El navegador realiza automáticamente comprobaciones de actualización cuando hay una navegación a una página nueva dentro del alcance de un service worker.

Cómo activar manualmente las verificaciones de actualizaciones

Con respecto a las actualizaciones, la lógica de registro generalmente no debería cambiar. Sin embargo, una excepción podría ser si las sesiones en un sitio web son de larga duración. Esto puede ocurrir en aplicaciones de una sola página en las que las solicitudes de navegación son poco frecuentes, ya que la aplicación suele encontrar una solicitud de navegación al inicio del ciclo de vida de la aplicación. En estas situaciones, se puede activar una actualización manual en el subproceso principal:

navigator.serviceWorker.ready.then((registration) => {
  registration.update();
});

En el caso de los sitios web tradicionales, o en cualquier caso en el que las sesiones de usuario no sean de larga duración, es probable que no sea necesario activar actualizaciones manuales.

Instalación

Cuando se usa un agrupador para generar recursos estáticos, estos contendrán hashes en su nombre, como framework.3defa9d2.js. Supongamos que algunos de esos recursos se almacenan previamente en caché para poder acceder a ellos sin conexión más adelante. Esto requeriría una actualización del service worker para almacenar previamente en caché los recursos actualizados:

self.addEventListener('install', (event) => {
  const cacheKey = 'MyFancyCacheName_v2';

  event.waitUntil(caches.open(cacheKey).then((cache) => {
    // Add all the assets in the array to the 'MyFancyCacheName_v2'
    // `Cache` instance for later use.
    return cache.addAll([
      '/css/global.ced4aef2.css',
      '/css/home.cbe409ad.css',
      '/js/home.109defa4.js',
      '/js/jquery.38caf32d.js'
    ]);
  }));
});

Hay dos diferencias respecto del primer ejemplo de evento install anterior:

  1. Se crea una instancia Cache nueva con una clave de 'MyFancyCacheName_v2'.
  2. Cambiaron los nombres de los recursos que se almacenaron previamente en caché.

Debes tener en cuenta que un service worker actualizado se instala junto con el anterior. Esto significa que el service worker anterior todavía tiene el control de las páginas abiertas y, después de la instalación, el nuevo entra en estado de espera hasta que se activa.

De forma predeterminada, se activará un nuevo service worker cuando el anterior no controle a ningún cliente. Esto ocurre cuando todas las pestañas abiertas del sitio web correspondiente están cerradas.

Activación

Cuando se instala un service worker actualizado y finaliza la fase de espera, se activa, y se descarta el service worker antiguo. Una tarea común que se debe realizar en el evento activate de un service worker actualizado es reducir las cachés antiguas. Para quitar las cachés antiguas, obtén las claves de todas las instancias de Cache abiertas con caches.keys y borra las cachés que no estén en una lista de entidades permitidas definida con caches.delete:

self.addEventListener('activate', (event) => {
  // Specify allowed cache keys
  const cacheAllowList = ['MyFancyCacheName_v2'];

  // Get all the currently active `Cache` instances.
  event.waitUntil(caches.keys().then((keys) => {
    // Delete all caches that aren't in the allow list:
    return Promise.all(keys.map((key) => {
      if (!cacheAllowList.includes(key)) {
        return caches.delete(key);
      }
    }));
  }));
});

Las memorias caché antiguas no se ordenan solas. Debemos hacerlo nosotros mismos o correr el riesgo de exceder las cuotas de almacenamiento. Dado que 'MyFancyCacheName_v1' del primer service worker está desactualizada, la lista de elementos permitidos de caché se actualiza para especificar 'MyFancyCacheName_v2', lo que borra las memorias caché con un nombre diferente.

El evento activate finalizará después de que se quite la caché anterior. En este punto, el nuevo service worker tomará el control de la página y, finalmente, reemplazará al anterior.

El ciclo de vida continúa

Vale la pena comprender el ciclo de vida del service worker, ya sea que se use Workbox o la API de Service Worker directamente para controlar su implementación y actualizaciones. Con esa comprensión, los comportamientos de los service worker deberían parecer más lógicos que misteriosos.

Para aquellos interesados en profundizar en este tema, vale la pena consultar este artículo de Jake Archibald. Hay muchos matices en la forma en que se completa todo el ciclo de vida del servicio, pero se puede conocer, y ese conocimiento llegará lejos cuando se use Workbox.