Guía de almacenamiento en caché imperativo

Andrew Guan
Andrew Guan
Demián Renzulli
Demián Renzulli

Es posible que algunos sitios web deban comunicarse con el trabajador de servicio sin necesidad de que se les informe sobre el resultado. Estos son algunos ejemplos:

  • Una página envía al trabajador de servicio una lista de URLs para la precarga, de modo que, cuando el usuario hace clic en un vínculo, los subrecursos del documento o la página ya están disponibles en la caché, lo que hace que la navegación posterior sea mucho más rápida.
  • La página le pide al service worker que recupere y almacene en caché un conjunto de artículos principales para que estén disponibles sin conexión.

Delegar estos tipos de tareas no críticas al trabajador de servicio tiene el beneficio de liberar el subproceso principal para controlar mejor las tareas más urgentes, como responder a las interacciones del usuario.

Diagrama de una página que solicita recursos para almacenar en caché en un service worker.

En esta guía, exploraremos cómo implementar una técnica de comunicación unidireccional desde la página al trabajador de servicio con las APIs de navegador estándar y la biblioteca de Workbox. Llamaremos a estos tipos de casos de uso caché imperativo.

Caso de producción

1-800-Flowers.com implementó el almacenamiento en caché imperativo (precarga) con trabajadores del servicio a través de postMessage() para precargar los elementos principales de las páginas de categorías y acelerar la navegación posterior a las páginas de detalles de los productos.

Logotipo de 1-800 Flowers.

Usan un enfoque mixto para decidir qué elementos recuperar previamente:

  • En el momento de la carga de la página, le pide al trabajador del servicio que recupere los datos JSON de los 9 elementos principales y que agregue los objetos de respuesta resultantes a la caché.
  • En el caso de los elementos restantes, escuchan el evento mouseover para que, cuando un usuario mueva el cursor sobre un elemento, pueda activar una recuperación del recurso "a pedido".

Usan la API de Cache para almacenar respuestas JSON:

Logotipo de 1-800 Flowers.
Precarga de datos de productos JSON de las páginas de fichas de productos en 1-800Flowers.com.

Cuando el usuario hace clic en un elemento, los datos JSON asociados con él se pueden recuperar de la caché, sin necesidad de ir a la red, lo que hace que la navegación sea más rápida.

Cómo usar Workbox

Workbox proporciona una forma sencilla de enviar mensajes a un trabajador de servicio a través del paquete workbox-window, un conjunto de módulos que se diseñaron para ejecutarse en el contexto de la ventana. Son un complemento de los otros paquetes de Workbox que se ejecutan en el trabajador de servicio.

Para comunicar la página con el trabajador de servicio, primero obtén una referencia de objeto de Workbox al trabajador de servicio registrado:

const wb = new Workbox('/sw.js');
wb.register();

Luego, puedes enviar el mensaje directamente de forma declarativa, sin la molestia de obtener el registro, verificar la activación ni pensar en la API de comunicación subyacente:

wb.messageSW({"type": "PREFETCH", "payload": {"urls": ["/data1.json", "data2.json"]}}); });

El trabajador de servicio implementa un controlador message para escuchar estos mensajes. De manera opcional, puede mostrar una respuesta, aunque, en casos como estos, no es necesario:

self.addEventListener('message', (event) => {
  if (event.data && event.data.type === 'PREFETCH') {
    // do something
  }
});

Cómo usar las APIs del navegador

Si la biblioteca de Workbox no es suficiente para tus necesidades, aquí te mostramos cómo implementar la comunicación del trabajador de ventana a servicio con las APIs del navegador.

La API de postMessage se puede usar para establecer un mecanismo de comunicación unidireccional desde la página al trabajador de servicio.

La página llama a postMessage() en la interfaz del servicio de trabajo:

navigator.serviceWorker.controller.postMessage({
  type: 'MSG_ID',
  payload: 'some data to perform the task',
});

El trabajador de servicio implementa un controlador message para escuchar estos mensajes.

self.addEventListener('message', (event) => {
  if (event.data && event.data.type === MSG_ID) {
    // do something
  }
});

El atributo {type : 'MSG_ID'} no es absolutamente necesario, pero es una forma de permitir que la página envíe diferentes tipos de instrucciones al trabajador de servicio (es decir, "para recuperar previamente" en comparación con "para borrar el almacenamiento"). El trabajador del servicio puede bifurcarse en diferentes instrucciones de ejecución según esta marca.

Si la operación se realizó correctamente, el usuario podrá obtener los beneficios, pero, de lo contrario, no se alterará el flujo de usuarios principal. Por ejemplo, cuando 1-800-Flowers.com intenta almacenar en caché previamente, la página no necesita saber si el trabajador de servicio tuvo éxito. Si es así, el usuario disfrutará de una navegación más rápida. Si no es así, la página aún debe navegar a la página nueva. Solo tardará un poco más.

Ejemplo simple de la carga previa

Una de las aplicaciones más comunes de la caché imperativa es la recuperación anticipada, que consiste en recuperar recursos para una URL determinada antes de que el usuario se dirija a ella para acelerar la navegación.

Existen diferentes formas de implementar la carga previa en los sitios:

Para situaciones de carga previa relativamente simples, como la carga previa de documentos o recursos específicos (JS, CSS, etc.), esas técnicas son el mejor enfoque.

Si se requiere lógica adicional, por ejemplo, analizar el recurso de precarga (un archivo o una página JSON) para recuperar sus URLs internas, es más apropiado delegar esta tarea por completo al trabajador de servicio.

Delegar estos tipos de operaciones al trabajador de servicio tiene las siguientes ventajas:

  • Descargar el trabajo pesado de la recuperación y el procesamiento posterior a la recuperación (que se presentará más adelante) a un subproceso secundario De esta manera, se libera el subproceso principal para que controle tareas más importantes, como responder a las interacciones del usuario.
  • Permite que varios clientes (p.ej., pestañas) reutilicen una funcionalidad común y, además, llamen al servicio de forma simultánea sin bloquear el subproceso principal.

Precarga de páginas de detalles del producto

Primero, usa postMessage() en la interfaz del trabajador de servicio y pasa un array de URLs para almacenar en caché:

navigator.serviceWorker.controller.postMessage({
  type: 'PREFETCH',
  payload: {
    urls: [
      'www.exmaple.com/apis/data_1.json',
      'www.exmaple.com/apis/data_2.json',
    ],
  },
});

En el trabajador de servicio, implementa un controlador message para interceptar y procesar los mensajes que envía cualquier pestaña activa:

addEventListener('message', (event) => {
  let data = event.data;
  if (data && data.type === 'PREFETCH') {
    let urls = data.payload.urls;
    for (let i in urls) {
      fetchAsync(urls[i]);
    }
  }
});

En el código anterior, presentamos una pequeña función auxiliar llamada fetchAsync() para iterar en el array de URLs y emitir una solicitud de recuperación para cada una de ellas:

async function fetchAsync(url) {
  // await response of fetch call
  let prefetched = await fetch(url);
  // (optionally) cache resources in the service worker storage
}

Cuando se obtiene la respuesta, puedes confiar en los encabezados de almacenamiento en caché del recurso. Sin embargo, en muchos casos, como en las páginas de detalles de productos, los recursos no se almacenan en caché (es decir, tienen un encabezado Cache-control de no-cache). En estos casos, puedes anular este comportamiento almacenando el recurso recuperado en la caché del trabajador del servicio. Esto tiene el beneficio adicional de permitir que el archivo se entregue en situaciones sin conexión.

Más allá de los datos JSON

Una vez que se recuperan los datos JSON de un extremo del servidor, a menudo contienen otras URLs que también vale la pena recuperar previamente, como una imagen o algún otro dato de extremo que esté asociado con estos datos de primer nivel.

Supongamos que, en nuestro ejemplo, los datos JSON que se muestran son la información de un sitio de compras de comestibles:

{
  "productName": "banana",
  "productPic": "https://cdn.example.com/product_images/banana.jpeg",
  "unitPrice": "1.99"
 }

Modifica el código fetchAsync() para iterar en la lista de productos y almacenar en caché la imagen hero de cada uno de ellos:

async function fetchAsync(url, postProcess) {
  // await response of fetch call
  let prefetched = await fetch(url);

  //(optionally) cache resource in the service worker cache

  // carry out the post fetch process if supplied
  if (postProcess) {
    await postProcess(prefetched);
  }
}

async function postProcess(prefetched) {
  let productJson = await prefetched.json();
  if (productJson && productJson.product_pic) {
    fetchAsync(productJson.product_pic);
  }
}

Puedes agregar un control de excepciones alrededor de este código para situaciones como los errores 404. Sin embargo, la ventaja de usar un trabajador de servicio para la carga previa es que puede fallar sin muchas consecuencias para la página y el subproceso principal. También puedes tener una lógica más elaborada en el procesamiento posterior del contenido almacenado previamente, lo que lo hace más flexible y desvinculado de los datos que controla. Todo es posible.

Conclusión

En este artículo, analizamos un caso de uso común de la comunicación unidireccional entre la página y el trabajador de servicio: la caché imperativa. Los ejemplos que se analizaron solo tienen como objetivo demostrar una forma de usar este patrón, y el mismo enfoque también se puede aplicar a otros casos de uso, por ejemplo, almacenar en caché los artículos principales a pedido para el consumo sin conexión, la creación de favoritos y otros.

Para obtener más patrones de comunicación entre la página y el trabajador de servicio, consulta lo siguiente:

  • Actualizaciones de transmisión: Llamar a la página desde el servicio trabajador para informar sobre actualizaciones importantes (p. ej., hay una versión nueva de la aplicación web disponible)
  • Comunicación bidireccional: Delega una tarea a un trabajador de servicio (p.ej., una descarga pesada) y mantén a la página informada sobre el progreso.