Guía de almacenamiento en caché imperativo

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

Es posible que algunos sitios web necesiten comunicarse con el service worker sin necesidad de estar informado sobre el resultado. Estos son algunos ejemplos:

  • Una página envía al service worker una lista de URLs que deben cargarse previamente, de modo que, cuando el usuario hace clic en un vínculo, los subrecursos del documento o de la página ya están disponibles en la caché, lo que agiliza la navegación posterior.
  • 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 service worker 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é a un service worker.

En esta guía, exploraremos cómo implementar una técnica de comunicación unidireccional desde la página hasta el service worker mediante el uso de APIs estándar del navegador y la biblioteca de Workbox. A estos tipos de casos de uso los llamaremos almacenamiento en caché imperativo.

Caso de producción

1-800-Flowers.com implementó el almacenamiento en caché imperativo (carga previa) con service worker a través de postMessage() para cargar previamente los elementos principales en páginas de categorías y, así, acelerar la navegación posterior a las páginas de detalles del producto.

Logotipo de 1-800 Flowers.

Usan un enfoque mixto para decidir qué elementos cargar con anticipación:

  • En el tiempo de carga de la página, le piden al servicer worker que recupere los datos JSON de los 9 elementos principales y que agregue los objetos de respuesta resultantes a la caché.
  • Para los elementos restantes, escuchan el evento mouseover de modo 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 de JSON:

Logotipo de 1-800 Flowers.
Recuperación previa de datos de productos JSON de páginas de fichas de productos en 1-800Flowers.com.

Cuando el usuario hace clic en un elemento, los datos JSON asociados a él se pueden obtener de la caché, sin necesidad de ir a la red, lo que agiliza la navegación.

Usa Workbox

Workbox proporciona una forma fácil de enviar mensajes a un service worker mediante el paquete workbox-window, un conjunto de módulos diseñados para ejecutarse en el contexto de la ventana. Son un complemento de los otros paquetes de Workbox que se ejecutan en el service worker.

Para comunicar la página con el service worker, primero obtén una referencia del objeto de Workbox al service worker 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 o pensar en la API de comunicación subyacente:

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

El service worker 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
  }
});

Uso de las APIs del navegador

Si la biblioteca de Workbox no es suficiente para tus necesidades, aquí se muestra cómo puedes implementar la comunicación de ventana a service worker mediante las APIs de navegador.

La API de postMessage se puede usar para establecer un mecanismo de comunicación unidireccional entre la página y el service worker.

La página llama a postMessage() en la interfaz de service worker:

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

El service worker 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 obligatorio, pero es una forma de permitir que la página envíe diferentes tipos de instrucciones al service worker (es decir, "cargar previamente" frente a "liberar almacenamiento"). El service worker puede ramificarse en diferentes rutas de ejecución en función de esta marca.

Si la operación fue exitosa, el usuario podrá obtener los beneficios de ella, pero, de no ser así, no alterará el flujo de usuario principal. Por ejemplo, cuando 1-800-Flowers.com intenta almacenar en caché previamente, la página no necesita saber si el service worker tuvo éxito. Si es así, el usuario disfrutará de una navegación más rápida. Si no funciona, la página debe navegar a la página nueva. Solo tomará un poco más de tiempo.

Un ejemplo simple de carga previa

Una de las aplicaciones más comunes del almacenamiento en caché imperativo es la carga previa, es decir, la recuperación de recursos para una URL determinada antes de que el usuario acceda a ella para acelerar la navegación.

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

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

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

Delegar estos tipos de operaciones al service worker tiene las siguientes ventajas:

  • Transferir el trabajo pesado del procesamiento de recuperación y posterior a la recuperación (que se presentará más adelante) a un subproceso secundario De esta manera, liberas el subproceso principal para que se encargue de 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 que incluso llamen al servicio de forma simultánea sin bloquear el subproceso principal.

Realiza cargas previas de las páginas de detalles de los productos.

Primero, usa postMessage() en la interfaz del service worker 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 service worker, implementa un controlador message para interceptar y procesar los mensajes enviados por 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 del producto, los recursos no se almacenan en caché (lo que significa que tienen un encabezado Cache-control de no-cache). En estos casos, puedes anular este comportamiento si almacenas el recurso recuperado en la caché del service worker. 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 los datos JSON se recuperan de un extremo del servidor, a menudo contienen otras URLs que también vale la pena cargar 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 compra de alimentos:

{
  "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 algún control de excepciones en torno a este código para situaciones como los errores 404. Sin embargo, lo bueno de usar un service worker para la carga previa es que puede fallar sin consecuencias para la página y el subproceso principal. También es posible que tengas una lógica más elaborada en el procesamiento posterior del contenido cargado previamente, lo que lo hace más flexible y separado de los datos que controla. Todo es posible.

Conclusión

En este artículo, abordamos un caso de uso común de comunicación unidireccional entre la página y el service worker: el almacenamiento en caché imperativo. Los ejemplos que se analizan solo se pretenden 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 su consumo sin conexión, agregarlos a favoritos y otros.

Para conocer más patrones de comunicación entre páginas y service worker, consulta:

  • Transmisión de actualizaciones: Llamar a la página desde el service worker para informar sobre actualizaciones importantes (p.ej., hay una nueva versión de webapp disponible).
  • Comunicación bidireccional: Delegar una tarea a un service worker (p.ej., una descarga pesada) y mantener a la página informada sobre el progreso.