El ciclo de vida del service worker es su parte más complicada. Si no sabes qué intenta hacer y cuáles son los beneficios, puede parecer que está luchando contra ti. Sin embargo, 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 las viñetas al comienzo de cada sección abarcan la mayor parte de lo que necesitas saber.
La intención
El objetivo del ciclo de vida es el siguiente:
- Hacer posible el funcionamiento sin conexión
- Permite que un nuevo service worker 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.
Ese último punto es muy importante. Sin service workers, 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 se trata de almacenamiento, puedes terminar fácilmente con dos pestañas que tengan opiniones muy diferentes sobre cómo se debe administrar su almacenamiento compartido. Esto puede provocar errores o, lo que es peor, pérdida de datos.
El primer service worker
En resumen:
- El evento
installes el primero que recibe un service worker 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 service worker no recibirá eventos como
fetchypushhasta que termine de instalarse 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 haya pasado por uno. Por lo tanto, deberás actualizar la página para ver los efectos del service worker.
clients.claim()puede anular este valor predeterminado y tomar el control de las páginas no controladas.
Toma este 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 service worker y 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é 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. Actualiza la página y 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, tiene un alcance predeterminado de //example.com/foo/.
Llamamos clients a las páginas, los trabajadores y los trabajadores compartidos. Tu service worker solo puede controlar los 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 un cliente se controla a través de navigator.serviceWorker.controller, que será nulo o una instancia de service worker.
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, la promesa de registro se rechaza y se descarta el service worker.
Las Herramientas para desarrolladores de Chrome muestran el error en la consola y en la sección del service worker de la pestaña de la aplicación:
Instalar
El primer evento que recibe un service worker es install. Se activa en cuanto se ejecuta el service worker y solo se llama una vez por service worker. Si modificas la secuencia de comandos del service worker, el navegador lo considerará un service worker diferente y obtendrá su propio evento install. Más adelante, abordaré las actualizaciones en detalle.
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 la promesa, esto indica que la instalación falló y el navegador descarta el service worker. Nunca controlará a los clientes. Esto significa que podemos confiar en que cat.svg estará presente en la caché en nuestros eventos fetch. Es una dependencia.
Activar
Una vez que el service worker esté listo para controlar clientes y controlar eventos funcionales como push y sync, recibirás un evento activate. Sin embargo, eso no significa que se controlará la página que llamó a .register().
La primera vez que cargas la demostración, aunque se solicita dog.svg mucho después de que se activa el service worker, este no 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 recursos secundarios. 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 los eventos fetch, y verás un gato en su lugar.
clients.claim
Puedes tomar el control de los clientes no controlados llamando a clients.claim() dentro de tu service worker una vez que se active.
Aquí tienes 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 depende del tiempo. Solo verás un gato si el service worker se activa y clients.claim() se aplica antes de que se intente cargar la imagen.
Si usas tu service worker para cargar páginas de manera diferente a como se cargarían a través de la red, clients.claim() puede ser problemático, ya que tu 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
pushysync, a menos que se haya realizado una verificación de actualizaciones en las últimas 24 horas - Llamar a
.register()solo si cambió 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é cuando buscan actualizaciones de la secuencia de comandos del service worker registrado. Sin embargo, respetan los encabezados de almacenamiento en caché cuando recuperan recursos cargados dentro de un service worker a través de
importScripts(). Puedes anular este comportamiento predeterminado si configuras la opciónupdateViaCachecuando registras tu service worker. - Se considera que tu service worker está actualizado si es diferente en bytes del que ya tiene el navegador. (También extenderemos esto para incluir secuencias de comandos y módulos importados).
- El service worker actualizado se inicia junto con el existente y obtiene su propio evento
install. - Si tu nuevo worker 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, pero el actual permanece activo.
- Una vez que se instale correctamente, el trabajador actualizado se
waithasta que el trabajador existente controle cero clientes. (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 cambiamos la secuencia de comandos de nuestro service worker para que responda con la 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. 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 service worker anterior aún está usando.
Este patrón crea cachés específicas de la versión, similares a los recursos que una aplicación 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 que se instala correctamente, el service worker actualizado retrasa la activación hasta que el service worker existente ya no controle a los clientes. Este estado se denomina "en espera" y es la forma en que el navegador garantiza que solo se ejecute una versión de tu service worker a la vez.
Si ejecutaste la demostración actualizada, deberías seguir viendo la imagen de un gato, ya que el trabajador de la versión 2 aún no se activó. Puedes ver el nuevo service worker en espera en la pestaña "Application" de DevTools:
Incluso si solo tienes una pestaña abierta en la demostración, actualizar la página no es suficiente para que la nueva versión tome el control. Esto se debe a cómo funcionan las navegaciones del navegador. Cuando navegas, la página actual no desaparece hasta que se reciben los encabezados de respuesta, y, aun así, la página actual puede permanecer 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 todas las pestañas que usen el service worker actual o navega fuera de ellas. Luego, cuando vuelvas a navegar a la demostración, deberías ver el caballo.
Este patrón es similar al modo 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 las Herramientas para desarrolladores tienen formas de facilitarlo, que abordaré más adelante en este artículo.
Activar
Esto se activa una vez que el service worker anterior desaparece y el nuevo puede controlar a los clientes. Este es el momento ideal para realizar tareas que no podías hacer mientras el trabajador anterior aún estaba 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 deshago 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 activa tu evento fetch, la activación se completa 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 service worker se active antes llamando a self.skipWaiting().
Esto hace que tu service worker expulse al worker 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 tu trabajador omita la instalación, solo espera.
No importa cuándo llames a skipWaiting(), siempre que sea durante o antes de la espera. Es bastante común llamarlo 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. Es decir, quieres skipWaiting() después de una interacción del usuario.
Aquí tienes 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 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 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 service worker
Si leíste mi publicación sobre las prácticas recomendadas para el almacenamiento en caché, puedes considerar asignarle a cada versión de tu service worker una URL única. No hagas esto Por lo general, esta es una mala práctica para los service workers. Solo actualiza la secuencia de comandos en su ubicación actual.
Esto puede generar un problema como el siguiente:
index.htmlregistrasw-v1.jscomo un service worker.sw-v1.jsalmacena en caché y entregaindex.htmlpara que funcione primero sin conexión.- Actualizas
index.htmlpara que registre tu nuevo y relucientesw-v2.js.
Si haces lo anterior, el usuario nunca obtiene sw-v2.js, porque sw-v1.js entrega la versión anterior de index.html desde su caché. Te pusiste en una posición en la que necesitas actualizar tu service worker para actualizarlo. Puaj.
Sin embargo, para la demostración anterior, cambié la URL del service worker. Esto es para que, por el bien de la demostración, puedas cambiar entre las versiones. No es algo que haría en producción.
Facilita el desarrollo
El ciclo de vida del service worker se crea pensando en el usuario, pero durante el desarrollo es un poco complicado. Por suerte, existen algunas herramientas que pueden ayudarte:
Actualizar al volver a cargar
Esta es mi favorita.
Esto cambia el ciclo de vida para que sea más fácil de usar para los desarrolladores. Cada navegación hará lo siguiente:
- Vuelve a recuperar el service worker.
- Instálalo como una versión nueva, incluso si es idéntico en bytes, lo que significa que se ejecuta tu evento
instally se actualizan tus cachés. - Omitir la fase de espera para que se active el nuevo service worker
- 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 cargar dos veces ni cerrar la pestaña.
No esperes
Si tienes un trabajador en espera, puedes hacer clic en "omitir espera" en las Herramientas para desarrolladores para promoverlo de inmediato a "activo".
Mayúsculas + Volver a cargar
Si fuerzas la recarga de la página (shift-reload), se omite por completo el service worker. No se podrá controlar. Esta función se encuentra en la especificación, por lo que funciona en otros navegadores que admiten service workers.
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 en el desarrollo web que los desarrolladores web. Por lo tanto, no deberíamos proporcionar APIs de alto nivel limitadas que resuelvan un problema en particular con patrones que nos gusten, sino que deberíamos darte acceso a las entrañas del navegador y permitirte hacerlo como quieras, de la manera que mejor funcione para tus usuarios.
Por lo tanto, para habilitar la mayor cantidad posible de patrones, 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 service worker y, con ese conocimiento, los comportamientos de los service workers 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 workers.