Mentalidad de service worker

Cómo pensar cuando se piensa en los service workers

Los Service Workers son potentes y vale la pena aprender a usarlos. Te permiten ofrecer un nivel de experiencia completamente nuevo a tus usuarios. Tu sitio puede cargarse al instante. Puede funcionar sin conexión. Se puede instalar como una app específica de la plataforma y se siente tan pulida, pero con el alcance y la libertad de la Web.

Sin embargo, los trabajadores de servicio no se parecen a nada a lo que la mayoría de los desarrolladores web están acostumbrados. Tienen una curva de aprendizaje pronunciada y algunos inconvenientes que debes tener en cuenta.

Recientemente, Google Developers y yo colaboramos en un proyecto: Service Workies, un juego gratuito para comprender los trabajadores de servicio. Mientras lo compilaba y trabajaba con los complejos detalles de los trabajadores del servicio, me encontré con algunos inconvenientes. Lo que más me ayudó fue idear algunas metáforas descriptivas. En esta publicación, exploraremos estos modelos mentales y analizaremos las características paradójicas que hacen que los trabajadores de servicio sean tan difíciles como increíbles.

Lo mismo, pero diferente

Mientras codificas tu trabajador de servicio, muchas cosas te resultarán familiares. Puedes usar tus funciones favoritas del nuevo lenguaje de JavaScript. Escuchas los eventos de ciclo de vida de la misma manera que los eventos de la IU. Administras el flujo de control con promesas como de costumbre.

Sin embargo, otros comportamientos del servicio de trabajo te hacen sentir confundido. Especialmente cuando actualizas la página y no ves los cambios de código aplicados.

Una capa nueva

Por lo general, cuando se crea un sitio, solo debes tener en cuenta dos capas: el cliente y el servidor. El service worker es una capa nueva que se encuentra en el medio.

Un trabajador de servicio actúa como una capa intermedia entre el cliente y el servidor.

Piensa en tu service worker como una especie de extensión del navegador, que tu sitio puede instalar en el navegador del usuario. Una vez instalado, el service worker extiende el navegador de tu sitio con una capa intermedia potente. Esta capa de service worker puede interceptar y controlar todas las solicitudes que realiza tu sitio.

La capa de service worker tiene su propio ciclo de vida independiente de la pestaña del navegador. Una simple actualización de página no es suficiente para actualizar un service worker, al igual que no esperarías que una actualización de página actualice el código implementado en un servidor. Cada capa tiene sus propias reglas únicas para la actualización.

En el juego Service Workies, abordamos los muchos detalles del ciclo de vida del service worker y te damos mucha práctica para trabajar con él.

Potente, pero limitado

Tener un service worker en tu sitio te brinda beneficios increíbles. Tu sitio puede hacer lo siguiente:

A pesar de todo lo que pueden hacer los trabajadores del servicio, están limitados por diseño. No pueden hacer nada de forma síncrona ni en el mismo subproceso que tu sitio. Esto significa que no tendrás acceso a lo siguiente:

  • localStorage
  • el DOM
  • la ventana

La buena noticia es que hay varias formas en que tu página puede comunicarse con su trabajador de servicio, como postMessage directo, canales de mensajes uno a uno y canales de transmisión uno a muchos.

De larga duración, pero de corta duración

Un service worker activo sigue activo incluso después de que un usuario sale de tu sitio o cierra la pestaña. El navegador mantiene este trabajador de servicio para que esté listo la próxima vez que el usuario vuelva a tu sitio. Antes de que se realice la primera solicitud, el service worker tiene la oportunidad de interceptarla y tomar el control de la página. Esto es lo que permite que un sitio funcione sin conexión: el service worker puede entregar una versión almacenada en caché de la página, incluso si el usuario no tiene conexión a Internet.

En Service Workies, visualizamos este concepto con Kolohe (un service worker amigable) que intercepta y controla las solicitudes.

Detenida

A pesar de que los service workers parecen ser inmortales, se pueden detener en casi cualquier momento. El navegador no quiere desperdiciar recursos en un service worker que no está haciendo nada en este momento. Detenerse no es lo mismo que finalizar: el service worker permanece instalado y activado. Solo se pone en suspensión. La próxima vez que se necesite (p.ej., para controlar una solicitud), el navegador lo reactivará.

waitUntil

Debido a la posibilidad constante de que se suspenda, tu trabajador de servicio necesita una forma de informarle al navegador cuando está haciendo algo importante y no quiere tomar una siesta. Aquí es donde entra en juego event.waitUntil(). Este método extiende el ciclo de vida en el que se usa, lo que evita que se detenga y que avance a la siguiente fase de su ciclo de vida hasta que estemos listos. Esto nos da tiempo para configurar cachés, recuperar recursos de la red, etcétera.

En este ejemplo, se le indica al navegador que nuestro service worker no se instalará hasta que se haya creado la caché de assets y se haya propagado con la imagen de una espada:

self.addEventListener("install", event => {
  event.waitUntil(
    caches.open("assets").then(cache => {
      return cache.addAll(["/weapons/sword/blade.png"]);
    })
  );
});

Ten cuidado con el estado global

Cuando se produce este inicio o detención, se restablece el alcance global del service worker. Por lo tanto, ten cuidado de no usar ningún estado global en tu servicio de trabajo, o te arrepentirás la próxima vez que se active y tenga un estado diferente del esperado.

Considera este ejemplo que usa un estado global:

const favoriteNumber = Math.random();
let hasHandledARequest = false;

self.addEventListener("fetch", event => {
  console.log(favoriteNumber);
  console.log(hasHandledARequest);
  hasHandledARequest = true;
});

En cada solicitud, este trabajador de servicio registrará un número, digamos 0.13981866382421893. La variable hasHandledARequest también cambia a true. Ahora, el service worker permanece inactivo durante un tiempo, por lo que el navegador lo detiene. La próxima vez que haya una solicitud, se necesitará el trabajador de servicio nuevamente, por lo que el navegador lo activará. Su secuencia de comandos se vuelve a evaluar. Ahora, hasHandledARequest se restablece a false y favoriteNumber es algo completamente diferente: 0.5907281835659033.

No puedes confiar en el estado almacenado en un trabajador de servicio. Además, crear instancias de elementos como los canales de mensajes puede causar errores: obtendrás una instancia nueva cada vez que se detenga o inicie el trabajador de servicio.

En el capítulo 3 de Service Workers, visualizamos que nuestro service worker detenido pierde todo el color mientras espera que se active.

Visualización de un trabajador de servicio detenido

Juntos, pero separados

Solo un service worker puede controlar tu página a la vez. Sin embargo, puede tener dos trabajadores de servicio instalados a la vez. Cuando haces un cambio en el código de tu service worker y actualizas la página, en realidad no estás editando el service worker. Los service workers son inmutables. En cambio, crearás uno nuevo. Este nuevo trabajador de servicio (llamemos SW2) se instalará, pero aún no se activará. Debe wait a que finalice el service worker actual (SW1) (cuando el usuario abandona tu sitio).

Interferir con las cachés de otro trabajador de servicio

Durante la instalación, SW2 puede configurar los elementos, por lo general, creando y completando cachés. Pero ten en cuenta que este nuevo service worker tiene acceso a todo a lo que tiene acceso el service worker actual. Si no tienes cuidado, el nuevo service worker en espera puede causar problemas en el service worker actual. Estos son algunos ejemplos que podrían causarte problemas:

  • SW2 podría borrar una caché que SW1 está usando de forma activa.
  • SW2 podría editar el contenido de una caché que usa SW1, lo que provocaría que SW1 responda con recursos que la página no espera.

Omitir skipWaiting

Un service worker también puede usar el método skipWaiting() riesgoso para tomar el control de la página en cuanto se termina de instalar. Por lo general, no es una buena idea, a menos que intentes reemplazar intencionalmente un trabajador de servicio con errores. Es posible que el nuevo service worker use recursos actualizados que la página actual no espera, lo que genera errores.

Cómo comenzar con un entorno limpio

La forma de evitar que tus trabajadores del servicio se superpongan es asegurarte de que usen diferentes cachés. La forma más sencilla de hacerlo es crear versiones de los nombres de caché que usan.

const version = 1;
const assetCacheName = `assets-${version}`;

self.addEventListener("install", event => {
  caches.open(assetCacheName).then(cache => {
    // confidently do stuff with your very own cache
  });
});

Cuando implementes un nuevo service worker, actualizarás el version para que haga lo que necesite con una caché completamente independiente del service worker anterior.

visualización de una caché

Finaliza la limpieza

Una vez que tu service worker alcanza el estado activated, sabes que se hizo cargo y que el service worker anterior es redundante. En este punto, es importante limpiar el proceso de trabajo anterior. No solo respeta los límites de almacenamiento en caché de tus usuarios, sino que también puede evitar errores no deseados.

El método caches.match() es un atajo que se usa con frecuencia para recuperar un elemento de cualquier caché en la que haya una coincidencia. Sin embargo, itera a través de las cachés en el orden en que se crearon. Supongamos que tienes dos versiones de un archivo de secuencia de comandos app.js en dos cachés diferentes: assets-1 y assets-2. Tu página espera la secuencia de comandos más reciente que se almacena en assets-2. Sin embargo, si no borraste la caché anterior, caches.match('app.js') mostrará la anterior de assets-1 y, lo más probable, es que tu sitio falle.

Para limpiar los procesos de trabajo de servicio anteriores, solo debes borrar la caché que no necesite el nuevo proceso de trabajo de servicio:

const version = 2;
const assetCacheName = `assets-${version}`;

self.addEventListener("activate", event => {
  event.waitUntil(
    caches.keys().then(cacheNames => {
      return Promise.all(
        cacheNames.map(cacheName => {
          if (cacheName !== assetCacheName){
            return caches.delete(cacheName);
          }
        });
      );
    });
  );
});

Evitar que tus service workers se interfieran entre sí requiere un poco de trabajo y disciplina, pero vale la pena.

Mentalidad de service worker

Adoptar la mentalidad adecuada mientras piensas en los trabajadores de servicio te ayudará a crear el tuyo con confianza. Una vez que domines los conceptos, podrás crear experiencias increíbles para tus usuarios.

Si quieres comprender todo esto jugando un partido, estás de suerte. Juega a Service Workies, donde aprenderás a usar el service worker para derrotar a las bestias sin conexión.