El ciclo de vida del service worker

El ciclo de vida del trabajador de servicio es su parte más complicada. Si no sabes qué intenta hacer y cuáles son los beneficios, puede parecer que está en tu contra. Pero una vez que sepas cómo funciona, podrás ofrecer actualizaciones fluidas y discretas a los usuarios, combinando lo mejor de los patrones web y nativos.

Este es un análisis detallado, pero los puntos al comienzo de cada sección abarcan la mayor parte de lo que necesitas saber.

El intent

El objetivo del ciclo de vida es el siguiente:

  • Haz posible el uso sin conexión.
  • Permite que un nuevo trabajador de servicio se prepare sin interrumpir el actual.
  • Asegúrate de que el mismo service worker (o ningún service worker) controle una página dentro del alcance en todo momento.
  • Asegúrate de que solo se ejecute una versión de tu sitio a la vez.

El último es muy importante. Sin los trabajadores del servicio, los usuarios pueden cargar una pestaña en tu sitio y, luego, abrir otra. Esto puede provocar que se ejecuten dos versiones de tu sitio al mismo tiempo. A veces, esto está bien, pero si tienes problemas con el almacenamiento, es posible que termines con dos pestañas que tengan opiniones muy diferentes sobre cómo se debe administrar el almacenamiento compartido. Esto puede provocar errores o, lo que es peor, pérdida de datos.

El primer service worker

En resumen:

  • El evento install es el primero que recibe un trabajador de servicio y solo ocurre una vez.
  • Una promesa que se pasa a installEvent.waitUntil() indica la duración y el éxito o el fracaso de la instalación.
  • Un trabajador de servicio no recibirá eventos como fetch y push hasta que termine de instalarse correctamente y se vuelva "activo".
  • De forma predeterminada, las recuperaciones de una página no pasan por un service worker, a menos que la solicitud de la página haya pasado por un service worker. Por lo tanto, deberás actualizar la página para ver los efectos del trabajador de servicio.
  • clients.claim() puede anular este valor predeterminado y tomar el control de las páginas no controladas.

Toma este código HTML:

<!DOCTYPE html>
An image will appear here in 3 seconds:
<script>
  navigator.serviceWorker.register('/sw.js')
    .then(reg => console.log('SW registered!', reg))
    .catch(err => console.log('Boo!', err));

  setTimeout(() => {
    const img = new Image();
    img.src = '/dog.svg';
    document.body.appendChild(img);
  }, 3000);
</script>

Registra un trabajador de servicio y agrega la imagen de un perro después de 3 segundos.

Este es su trabajador de servicio, sw.js:

self.addEventListener('install', event => {
  console.log('V1 installing…');

  // cache a cat SVG
  event.waitUntil(
    caches.open('static-v1').then(cache => cache.add('/cat.svg'))
  );
});

self.addEventListener('activate', event => {
  console.log('V1 now ready to handle fetches!');
});

self.addEventListener('fetch', event => {
  const url = new URL(event.request.url);

  // serve the cat SVG from the cache if the request is
  // same-origin and the path is '/dog.svg'
  if (url.origin == location.origin && url.pathname == '/dog.svg') {
    event.respondWith(caches.match('/cat.svg'));
  }
});

Almacena en caché una imagen de un gato y la entrega cada vez que hay una solicitud de /dog.svg. Sin embargo, si ejecutas el ejemplo anterior, verás un perro la primera vez que cargues la página. Presiona Actualizar y verás al gato.

Alcance y control

El alcance predeterminado de un registro de trabajador de servicio es ./ en relación con la URL de la secuencia de comandos. Esto significa que, si registras un trabajador de servicio en //example.com/foo/bar.js, tendrá un alcance predeterminado de //example.com/foo/.

Llamamos clients a las páginas, los trabajadores y los trabajadores compartidos. Tu trabajador de servicio solo puede controlar los clientes que están dentro del alcance. Una vez que un cliente está "controlado", sus recuperaciones pasan por el trabajador del servicio dentro del alcance. Puedes detectar si un cliente se controla a través de navigator.serviceWorker.controller, que será nulo o una instancia de trabajador de servicio.

Descarga, analiza y ejecuta

Tu primer service worker se descarga cuando llamas a .register(). 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 trabajador del servicio.

Las herramientas para desarrolladores de Chrome muestran el error en la consola y en la sección del trabajador del servicio de la pestaña de la aplicación:

Error que se muestra en la pestaña de DevTools del trabajador de servicio

Instalar

El primer evento que recibe un trabajador de servicio es install. Se activa en cuanto se ejecuta el trabajador y solo se lo llama una vez por trabajador de servicio. Si alteras la secuencia de comandos del trabajador de servicio, el navegador lo considerará un trabajador de servicio diferente y obtendrá su propio evento install. Hablaremos de las actualizaciones en detalle más adelante.

El evento install es tu oportunidad de almacenar en caché todo lo que necesitas antes de poder controlar a los clientes. La promesa que pasas a event.waitUntil() le permite al navegador saber cuándo se completa la instalación y si se realizó correctamente.

Si se rechaza tu promesa, significa que la instalación falló y el navegador descarta el trabajador del servicio. Nunca controlará a los clientes. Esto significa que podemos confiar en que cat.svg esté presente en la caché en nuestros eventos fetch. Es una dependencia.

Activar

Una vez que tu trabajador de servicio esté listo para controlar clientes y controlar eventos funcionales, como push y sync, recibirás un evento activate. Pero eso no significa que se controlará la página que llamó a .register().

La primera vez que cargas la demo, aunque se solicite dog.svg mucho después de que se active el trabajador de servicio, no se controla la solicitud y sigues viendo la imagen del perro. El valor predeterminado es coherencia. Si tu página se carga sin un service worker, tampoco se cargarán sus subrecursos. Si cargas la demostración por segunda vez (es decir, actualizas la página), se controlará. Tanto la página como la imagen pasarán por eventos fetch y, en su lugar, verás un gato.

clients.claim

Para tomar el control de los clientes no controlados, llama a clients.claim() dentro de tu trabajador de servicio una vez que se active.

Esta es una variación de la demostración anterior que llama a clients.claim() en su evento activate. Deberías ver un gato la primera vez. Digo "debería" porque esto es sensible al tiempo. Solo verás un gato si se activa el trabajador de servicio y clients.claim() se aplica antes de que se intente cargar la imagen.

Si usas tu trabajador de servicio para cargar páginas de forma diferente a como lo harían a través de la red, clients.claim() puede ser problemático, ya que tu trabajador de servicio termina controlando algunos clientes que se cargaron sin él.

Actualiza el trabajador del servicio

En resumen:

  • Se activa una actualización si ocurre alguna de las siguientes situaciones:
    • Una navegación a una página dentro del alcance.
    • Eventos funcionales, como push y sync, a menos que se haya realizado una verificación de actualización en las 24 horas anteriores
    • Llamar a .register() solo si cambió la URL del service worker Sin embargo, evita cambiar la URL del trabajador.
  • La mayoría de los navegadores, incluido Chrome 68 y versiones posteriores, ignoran de forma predeterminada los encabezados de almacenamiento en caché cuando buscan actualizaciones de la secuencia de comandos del trabajador del servicio registrado. Aún respetan los encabezados de almacenamiento en caché cuando recuperan recursos cargados dentro de un service worker a través de importScripts(). Para anular este comportamiento predeterminado, configura la opción updateViaCache cuando registres tu trabajador de servicio.
  • Tu trabajador de servicio se considera actualizado si es diferente en bytes del que ya tiene el navegador. (También estamos ampliando esta función para incluir secuencias de comandos o módulos importados).
  • El service worker actualizado se inicia junto con el existente y obtiene su propio evento install.
  • Si el trabajador nuevo tiene un código de estado que no es correcto (por ejemplo, 404), no se analiza, arroja un error durante la ejecución o se rechaza durante la instalación, se descarta el trabajador nuevo, pero el actual permanece activo.
  • Una vez que se instale correctamente, el trabajador actualizado wait hasta que el trabajador existente no controle ningún cliente. (Ten en cuenta que los clientes se superponen durante una actualización).
  • self.skipWaiting() evita el tiempo de espera, lo que significa que el trabajador de servicio se activa en cuanto termina de instalarse.

Supongamos que cambiamos nuestra secuencia de comandos de trabajador de servicio para que responda con una imagen de un caballo en lugar de un gato:

const expectedCaches = ['static-v2'];

self.addEventListener('install', event => {
  console.log('V2 installing…');

  // cache a horse SVG into a new cache, static-v2
  event.waitUntil(
    caches.open('static-v2').then(cache => cache.add('/horse.svg'))
  );
});

self.addEventListener('activate', event => {
  // delete any caches that aren't in expectedCaches
  // which will get rid of static-v1
  event.waitUntil(
    caches.keys().then(keys => Promise.all(
      keys.map(key => {
        if (!expectedCaches.includes(key)) {
          return caches.delete(key);
        }
      })
    )).then(() => {
      console.log('V2 now ready to handle fetches!');
    })
  );
});

self.addEventListener('fetch', event => {
  const url = new URL(event.request.url);

  // serve the horse SVG from the cache if the request is
  // same-origin and the path is '/dog.svg'
  if (url.origin == location.origin && url.pathname == '/dog.svg') {
    event.respondWith(caches.match('/horse.svg'));
  }
});

Mira una demostración de lo anterior. Deberías seguir viendo la imagen de un gato. A continuación, te explicamos por qué:

Instalar

Ten en cuenta que cambié el nombre de la caché de static-v1 a static-v2. Esto significa que puedo configurar la nueva caché sin reemplazar elementos de la actual, que el servicio en segundo plano anterior sigue usando.

Estos patrones crean cachés específicas de la versión, similares a los recursos que una app nativa agruparía con su ejecutable. También puedes tener cachés que no sean específicas de la versión, como avatars.

Esperando

Después de instalarse correctamente, el service worker actualizado retrasa la activación hasta que el existente ya no controle a los clientes. Este estado se denomina "esperando" y es la forma en que el navegador se asegura de que solo se ejecute una versión de tu trabajador de servicio a la vez.

Si ejecutaste la demo actualizada, deberías seguir viendo una foto de un gato, ya que el trabajador de V2 aún no se activó. Puedes ver el nuevo trabajador de servicio en espera en la pestaña “Application” de DevTools:

DevTools muestra un nuevo trabajador de servicio en espera

Incluso si solo tienes una pestaña abierta para la demostración, actualizar la página no es suficiente para que se aplique la nueva versión. Esto se debe al funcionamiento de las navegaciones del navegador. Cuando navegas, la página actual no desaparece hasta que se reciben los encabezados de respuesta y, aun así, es posible que la página actual permanezca si la respuesta tiene un encabezado Content-Disposition. Debido a esta superposición, el service worker actual siempre controla un cliente durante una actualización.

Para obtener la actualización, cierra o sal de todas las pestañas con el trabajador del servicio actual. Luego, cuando navegues a la demostración nuevamente, deberías ver el caballo.

Este patrón es similar a la forma en que se actualiza Chrome. Las actualizaciones de Chrome se descargan en segundo plano, pero no se aplican hasta que se reinicia el navegador. Mientras tanto, puedes seguir usando la versión actual sin interrupciones. Sin embargo, esto es un problema durante el desarrollo, pero DevTools tiene formas de facilitarlo, que explicaré más adelante en este artículo.

Activar

Se activa una vez que desaparece el trabajador del servicio anterior y el nuevo puede controlar a los clientes. Este es el momento ideal para hacer tareas que no podías realizar mientras el trabajador anterior seguía en uso, como migrar bases de datos y borrar cachés.

En la demostración anterior, mantengo una lista de las cachés que espero que estén allí y, en el evento activate, me desago de las demás, lo que quita la caché static-v1 anterior.

Si pasas una promesa a event.waitUntil(), se almacenarán en búfer los eventos funcionales (fetch, push, sync, etc.) hasta que se resuelva la promesa. Por lo tanto, cuando se active el evento fetch, la activación se habrá completado por completo.

Omitir la fase de espera

La fase de espera significa que solo ejecutas una versión de tu sitio a la vez, pero si no necesitas esa función, puedes hacer que tu nuevo trabajador de servicio se active antes llamando a self.skipWaiting().

Esto hace que tu trabajador de servicio expulse al trabajador activo actual y se active en cuanto entre en la fase de espera (o de inmediato si ya está en la fase de espera). No hace que el trabajador omita la instalación, solo espera.

Realmente no importa cuándo llames a skipWaiting(), siempre y cuando lo hagas durante o antes de esperar. Es bastante común llamarlo en el evento install:

self.addEventListener('install', event => {
  self.skipWaiting();

  event.waitUntil(
    // caching etc
  );
});

Sin embargo, te recomendamos que lo llames como resultado de un postMessage() al trabajador de servicio. Es decir, quieres skipWaiting() después de una interacción del usuario.

Esta es una demostración que usa skipWaiting(). Deberías ver la imagen de una vaca sin tener que salir de la página. Al igual que clients.claim(), es una carrera, por lo que solo verás la vaca si el nuevo trabajador de servicio recupera, instala y activa la imagen antes de que la página intente cargarla.

Actualizaciones manuales

Como mencioné anteriormente, el navegador busca actualizaciones automáticamente después de las navegaciones y los eventos funcionales, pero también puedes activarlas de forma manual:

navigator.serviceWorker.register('/sw.js').then(reg => {
  // sometime later…
  reg.update();
});

Si esperas que el usuario use tu sitio durante mucho tiempo sin volver a cargarlo, te recomendamos que llames a update() en un intervalo (por ejemplo, cada hora).

Evita cambiar la URL de la secuencia de comandos de tu trabajador de servicio

Si leíste mi publicación sobre las prácticas recomendadas de almacenamiento en caché, te recomendamos que le asignes una URL única a cada versión de tu trabajador de servicio. No lo hagas. Por lo general, esta no es una práctica recomendada para los trabajadores de servicio. Solo actualiza la secuencia de comandos en su ubicación actual.

Puede generar un problema como el siguiente:

  1. index.html registra sw-v1.js como un trabajador de servicio.
  2. sw-v1.js almacena en caché y entrega index.html para que funcione sin conexión.
  3. Actualizas index.html para que registre tu sw-v2.js nuevo y reluciente.

Si haces lo anterior, el usuario nunca obtendrá sw-v2.js, ya que sw-v1.js entrega la versión anterior de index.html desde su caché. Te encontraste en una situación en la que debes actualizar el trabajador de servicio para actualizar el trabajador de servicio. Puaj.

Sin embargo, en la demostración anterior, cambie la URL del trabajador del servicio. Esto se hace para que, a modo de demostración, puedas cambiar entre las versiones. No es algo que haría en producción.

Simplifica el desarrollo

El ciclo de vida del trabajador de servicio se crea teniendo en cuenta al usuario, pero durante el desarrollo es un poco complicado. Por suerte, existen algunas herramientas que pueden ayudarte:

Actualización cuando se vuelva a cargar

Esta es mi favorita.

Herramientas para desarrolladores que muestran &quot;actualizar cuando se vuelva a cargar&quot;

Esto cambia el ciclo de vida para que sea más fácil para los desarrolladores. Cada navegación hará lo siguiente:

  1. Vuelve a recuperar el trabajador del servicio.
  2. Instálala como una versión nueva, incluso si es idéntica en bytes, lo que significa que se ejecutará tu evento install y se actualizarán tus cachés.
  3. Omite la fase de espera para que se active el nuevo trabajador de servicio.
  4. Navega por la página.

Esto significa que recibirás las actualizaciones en cada navegación (incluida la actualización) sin tener que volver a cargarlas dos veces ni cerrar la pestaña.

Omitir espera

Herramientas para desarrolladores que muestran la opción “Omitir espera”

Si tienes un trabajador en espera, puedes presionar "Omitir espera" en DevTools para promocionarlo de inmediato a "Activo".

Mayúsculas + Volver a cargar

Si vuelves a cargar la página de forma forzosa (cargar con Mayúsculas), se omitirá por completo el trabajador del servicio. No estará controlado. Esta función está en la especificación, por lo que funciona en otros navegadores compatibles con los trabajadores del servicio.

Cómo controlar las actualizaciones

El trabajador de servicio se diseñó como parte de la Web extensible. La idea es que, como desarrolladores de navegadores, reconocemos que no somos mejores que los desarrolladores web en el desarrollo web. Por lo tanto, no debemos proporcionar APIs de alto nivel limitadas que resuelvan un problema en particular con patrones que nos gustan, sino que debemos darte acceso a los elementos esenciales del navegador y permitirte hacerlo como quieras, de la manera que funcione mejor para tus usuarios.

Por lo tanto, para habilitar tantos patrones como sea posible, se puede observar todo el ciclo de actualización:

navigator.serviceWorker.register('/sw.js').then(reg => {
  reg.installing; // the installing worker, or undefined
  reg.waiting; // the waiting worker, or undefined
  reg.active; // the active worker, or undefined

  reg.addEventListener('updatefound', () => {
    // A wild service worker has appeared in reg.installing!
    const newWorker = reg.installing;

    newWorker.state;
    // "installing" - the install event has fired, but not yet complete
    // "installed"  - install complete
    // "activating" - the activate event has fired, but not yet complete
    // "activated"  - fully active
    // "redundant"  - discarded. Either failed install, or it's been
    //                replaced by a newer version

    newWorker.addEventListener('statechange', () => {
      // newWorker.state has changed
    });
  });
});

navigator.serviceWorker.addEventListener('controllerchange', () => {
  // This fires when the service worker controlling this page
  // changes, eg a new worker has skipped waiting and become
  // the new active worker.
});

El ciclo de vida continúa

Como puedes ver, vale la pena comprender el ciclo de vida del trabajador de servicio. Con esa comprensión, los comportamientos del trabajador de servicio deberían parecer más lógicos y menos misteriosos. Ese conocimiento te dará más confianza a medida que implementes y actualices los trabajadores del servicio.