El ciclo de vida del service worker

El ciclo de vida del service worker es la parte más complicada de este. Si desconoces lo que está tratando de hacer y cuáles son los beneficios, puede sentir que te está peleando. Pero una vez que sabes cómo funciona, puedes entregar actualizaciones fluidas y discretas a los usuarios, mezclando lo mejor de los patrones web y nativos.

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

El intent

La intención del ciclo de vida es la siguiente:

  • Haz posible la priorización del uso sin conexión.
  • Permitir que un nuevo service worker se prepare sin interrumpir el flujo del actual
  • Asegurarse de que una página dentro del alcance esté controlada por el mismo service worker (o por ningún service worker) en todo momento.
  • Asegúrate de que solo se ejecute una versión de tu sitio a la vez.

La última es muy importante. Sin service worker, los usuarios pueden cargar una pestaña en tu sitio y, luego, abrir otra. Como resultado, es posible que se ejecuten dos versiones de tu sitio al mismo tiempo. A veces, esto es correcto, pero si estás lidiando con el almacenamiento, puedes terminar fácilmente con dos pestañas con opiniones muy diferentes sobre cómo debe administrarse 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 primer evento que obtiene un service worker y solo sucede una vez.
  • Una promesa que se pasa a installEvent.waitUntil() indica la duración y el éxito o fracaso de la instalación.
  • Un service worker no recibirá eventos como fetch y push hasta que se termine de instalar correctamente y se vuelva "activo".
  • De forma predeterminada, las recuperaciones de una página no pasarán por 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 controladas.

Analiza 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>

Se registra un service worker y se agrega la imagen de un perro después de 3 segundos.

Este es su service worker, 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é la imagen de un gato y la entrega cada vez que haya una solicitud de /dog.svg. Sin embargo, si ejecutas el ejemplo anterior, verás un perro la primera vez que cargues la página. Si actualizas la página, verás el gato.

Alcance y control

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

Llamamos a las páginas, los trabajadores y los trabajadores compartidos clients. Tu service worker solo puede controlar clientes que estén dentro del alcance. Una vez que un cliente está “controlado”, sus recuperaciones pasan por el service worker dentro del alcance. Puedes detectar si se controla un cliente con navigator.serviceWorker.controller, ya que su valor será nulo o será una instancia de service worker.

Descargar, analizar y ejecutar

Cuando llamas a .register(), se descarga tu 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.

Las Herramientas para desarrolladores de Chrome muestran el error en la consola y en la sección de service worker de la pestaña de la aplicación:

Se muestra un error en la pestaña de Herramientas para desarrolladores del service worker

Instalar

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

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

Si se rechaza la promesa, significa que la instalación no se realizó y que el navegador descartará el service worker. Nunca controlará a los clientes. Esto significa que no podemos confiar en que cat.svg esté presente en la caché en nuestros eventos fetch. Es una dependencia.

Activación

Una vez que el 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 llamó a .register().

La primera vez que cargas la demostración, si bien dog.svg se solicita mucho después de que se activa el service worker, no se controla la solicitud y sigues viendo la imagen del perro. El valor predeterminado es coherencia. Si la página se carga sin un service worker, tampoco lo harán los 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

Puedes tomar el control de clientes no controlados llamando a clients.claim() en tu service worker una vez que esté activado.

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 es una cuestión de sincronización. Solo verás un gato si se activa el service worker y clients.claim() se aplica antes de que la imagen intente cargarse.

Si usas tu service worker para cargar páginas de manera diferente a como lo harían a través de la red, clients.claim() podría generar problemas, ya que el service worker termina controlando algunos clientes que se cargaron sin él.

Actualiza el service worker

En resumen:

  • Se activa una actualización si ocurre alguna de las siguientes situaciones:
    • Una navegación a una página dentro del alcance
    • Un evento funcional, como push y sync, a menos que haya habido una verificación de actualización en las últimas 24 horas
    • Llamar a .register() solo si la URL del service worker Sin embargo, debes evitar cambiar la URL del trabajador.
  • La mayoría de los navegadores, incluidos Chrome 68 y versiones posteriores, ignoran de forma predeterminada los encabezados de almacenamiento en caché al buscar actualizaciones de la secuencia de comandos del service worker registrada. Siguen respetando los encabezados de almacenamiento en caché cuando se recuperan recursos cargados en un service worker a través de importScripts(). Puedes anular este comportamiento predeterminado configurando la opción updateViaCache cuando registras tu service worker.
  • Tu service worker se considera actualizado si tiene una cantidad de bytes diferente con respecto al service worker que ya tiene el navegador. (Extendimos este concepto para incluir también las secuencias de comandos y los módulos importados).
  • El service worker actualizado se inicia junto con el existente y recibe su propio evento install.
  • Si tu nuevo trabajador tiene un código de estado incorrecto (por ejemplo, 404), no se analiza, arroja un error durante la ejecución o se rechaza durante la instalación, el nuevo trabajador se elimina, pero el actual permanece activo.
  • Una vez que se instale correctamente, el trabajador actualizado ejecutará 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 la espera, lo que significa que el service worker se activa en cuanto termina de instalarse.

Supongamos que modificamos la secuencia de comandos de nuestro service worker para responder con una imagen de un caballo en lugar de la 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 sobrescribir elementos en la actual, que sigue utilizando el service worker anterior.

Con este patrón, se crean cachés específicas de la versión, similar a los recursos que una app nativa incluiría en el paquete con su ejecutable. También es posible que tengas cachés que no son específicas de la versión, como avatars.

Esperando

Después de que el service worker actualizado se instala correctamente, no se activa hasta que el service worker actual ya no controle clientes. Este estado se denomina "esperando" y es la forma en que el navegador garantiza que solo se ejecute una versión de tu service worker a la vez.

Si ejecutas la demostración actualizada, deberías seguir viendo una imagen de un gato, ya que el trabajador V2 aún no se activó. Puedes ver el nuevo service worker en estado de espera en la pestaña "Application" de Herramientas para desarrolladores:

Herramientas para desarrolladores mostrando un nuevo service worker en espera

Incluso si solo tienes una pestaña abierta en la demostración, actualizar la página no es suficiente para permitir que la nueva versión tome el control. Esto se debe al funcionamiento de la navegación en el navegador. Cuando navegas, la página actual no desaparece hasta que se reciben los encabezados de respuesta, y aun entonces 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 abandona todas las pestañas que usan el service worker actual. Luego, cuando navegues hasta la demostración de nuevo, deberías ver el caballo.

Este patrón es similar a como se actualiza Chrome. Las actualizaciones de Chrome se descargan en segundo plano, pero no se aplican hasta que Chrome se reinicia. Mientras tanto, puedes continuar usando la versión actual sin interrupciones. Sin embargo, esto es un inconveniente durante el desarrollo, pero Herramientas para desarrolladores tiene formas de hacerlo más fácil, que analizaremos más adelante en este artículo.

Activación

Se activa cuando el service worker anterior desaparece y tu nuevo service worker puede controlar clientes. Este es el momento ideal para realizar tareas que no pudiste hacer mientras el trabajador anterior todavía estaba en uso, como por ejemplo, migrar bases de datos y vaciar cachés.

En la demostración anterior, mantengo una lista de cachés que espero que estén allí y, en el evento activate, elimino el resto, 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 activa el evento fetch, significa que se completó la activación.

Omitir la fase de espera

La fase de espera significa que solo puedes ejecutar una versión de tu sitio a la vez. Sin embargo, si no necesitas esa función, puedes hacer que el nuevo service worker se active antes llamando a self.skipWaiting().

De esta manera, el service worker expulsará el trabajador activo actual y se activará automáticamente apenas entre en la fase de espera (o de inmediato si ya está en esa fase). No hace que el trabajador omita la instalación; solo está esperando.

Puedes llamar a skipWaiting() en cualquier momento, siempre y cuando sea antes o durante la espera. Es bastante común llamarla en el evento install:

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

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

Sin embargo, es posible que quieras llamarla como resultado de un postMessage() al service worker. En este caso, quieres skipWaiting() después de una interacción del usuario.

Esta es una demostración que usa skipWaiting(). Deberías ver una 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 service worker recupera, instala y activa antes de que la página intente cargar la imagen.

Actualizaciones manuales

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

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

Si esperas que el usuario utilice tu sitio durante mucho tiempo sin volver a cargar la página, puedes establecer un intervalo de llamada a update() (por ejemplo, una hora).

Evita cambiar la URL de la secuencia de comandos del service worker

Si leíste mi publicación sobre las prácticas recomendadas sobre el almacenamiento en caché, puedes considerar la posibilidad de asignar una URL única a cada versión de tu service worker. No lo hagas. Por lo general, se trata de una práctica no recomendada para los service worker. Simplemente, actualiza la secuencia de comandos en su ubicación actual.

Te puede dar un problema como el siguiente:

  1. index.html registra sw-v1.js como service worker.
  2. sw-v1.js almacena en caché y entrega index.html, de modo que funcione en primer lugar sin conexión.
  3. Actualizas index.html para que registre tu nueva y brillante sw-v2.js.

Si haces lo anterior, el usuario nunca obtendrá sw-v2.js, porque sw-v1.js entrega la versión anterior de index.html desde su caché. Te encuentras en una posición en la que debes actualizar tu service worker para actualizar tu service worker. Puaj.

Sin embargo, para la demostración anterior, cambié la URL del service worker. En esta demostración, puedes alternar entre las versiones. No es algo que haga en producción.

Facilitamos el desarrollo

El ciclo de vida del service worker se crea teniendo en cuenta al usuario. Sin embargo, durante el desarrollo, este concepto es un punto débil. Afortunadamente, existen algunas herramientas que pueden resultarte útiles:

Actualizar cuando se vuelva a cargar

Esta es mi favorita.

Se muestra la herramienta &quot;update on reload&quot; en Herramientas para desarrolladores

Esto cambia el ciclo de vida para que sea fácil de usar para el desarrollador. Cada navegación hará lo siguiente:

  1. Vuelve a recuperar el service worker.
  2. Instálalo como una versión nueva, incluso si tiene la misma cantidad de bytes, lo que significa que se ejecutará el evento install y se actualizarán las cachés.
  3. Omite la fase de espera para que se active el nuevo service worker.
  4. Navega por la página.

Esto significa que recibirás actualizaciones en cada navegación (incluida la opción de actualizar) sin tener que volver a cargar la página dos veces ni cerrar la pestaña.

Omitir espera

Se muestra la función &quot;skipWaiting&quot; de las Herramientas para desarrolladores

Si tienes un trabajador en espera, puedes presionar "skip pending" en Herramientas para desarrolladores para pasarlo inmediatamente a estado "active".

Mayús-volver a cargar

Si fuerzas la recarga de la página (shift-reload), se omite el service worker por completo. No se lo controlará. Esta función se encuentra en la especificación, por lo que funciona en otros navegadores compatibles con el service worker.

Cómo controlar las actualizaciones

El service worker se diseñó como parte de la Web extensible. La idea es que nosotros, como desarrolladores de navegadores, reconozcamos que no somos mejores que los desarrolladores web en lo que respecta al desarrollo web. Por lo tanto, no debemos proporcionar APIs de alto nivel y estrechas que resuelvan un problema en particular mediante patrones que nos gusten. En su lugar, debemos darte acceso a la parte central del navegador y permitirte que lo hagas como quieras, de manera que funcione mejor para tus usuarios.

Por lo tanto, para habilitar la mayor cantidad posible de patrones, todo el ciclo de actualización es observable:

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, sirve para comprender el ciclo de vida de los service worker. Con esa comprensión, los comportamientos de los service worker deberían parecer más lógicos y menos misteriosos. Ese conocimiento te dará más confianza a medida que implementes y actualices los service worker.