Más allá de las SPA: arquitecturas alternativas para tu AWP

Hablemos de... ¿arquitectura?

Abordaré un tema importante que podría malinterpretarse: la arquitectura que usas para tu app web y, en particular, cómo entran en juego tus decisiones arquitectónicas cuando compilas una app web progresiva.

El término "arquitectura" puede sonar poco claro y es posible que no quede claro de inmediato por qué esto es importante. Para entender la arquitectura, puedes hacerte las siguientes preguntas: cuando un usuario visita una página de mi sitio, ¿qué HTML se carga? Luego, ¿qué se carga cuando visitan otra página?

Las respuestas a esas preguntas no siempre son sencillas y, una vez que comienzas a pensar en las apps web progresivas, pueden volverse aún más complicadas. Mi objetivo es mostrarte una posible arquitectura que me pareció efectiva. En este artículo, etiquetaré las decisiones que tomé como "mi enfoque" para compilar una app web progresiva.

Puedes usar mi enfoque cuando compiles tu propia AWP, pero, al mismo tiempo, siempre hay otras alternativas válidas. Espero que ver cómo todas las piezas encajan te inspire y que te sientas empoderado para personalizar esta función según tus necesidades.

AWP de Stack Overflow

Para acompañar este artículo, creé una AWP de Stack Overflow. Dedico mucho tiempo a leer y contribuir a Stack Overflow, y quería compilar una app web que facilitara la exploración de las preguntas frecuentes sobre un tema determinado. Está construido sobre la API de Stack Exchange pública. Es de código abierto, y puedes obtener más información si visitas el proyecto de GitHub.

Apps de varias páginas (MPA)

Antes de entrar en detalles específicos, definamos algunos términos y expliquemos las partes de la tecnología subyacente. Primero, abordaré lo que me gusta llamar "apps de varias páginas" o "MPA".

MPA es un nombre elegante para la arquitectura tradicional que se utiliza desde los inicios de la Web. Cada vez que un usuario navega a una URL nueva, el navegador procesa progresivamente el código HTML específico de esa página. No se intenta preservar el estado de la página ni el contenido entre navegaciones. Cada vez que visitas una página nueva, empiezas desde cero.

Esto contrasta con el modelo de app de una sola página (SPA) para compilar apps web, en el que el navegador ejecuta el código JavaScript para actualizar la página existente cuando el usuario visita una sección nueva. Tanto las SPA como las MPA son modelos válidos para usar, pero para esta publicación quería explorar conceptos de AWP dentro del contexto de una app de varias páginas.

Rápido y confiable

Me escuchaste (y muchas personas más) la frase "app web progresiva" o AWP. Es posible que ya conozcas el material de referencia, en otra sección de este sitio.

Puedes pensar en una AWP como una app web que proporciona una experiencia del usuario de primer nivel y que realmente gana un lugar en la pantalla principal del usuario. El acrónimo "FIRE", que significa Fast, Integrado, Reliable y Engaging, resume todos los atributos que se deben considerar cuando se compila una AWP.

En este artículo, nos enfocaremos en un subconjunto de esos atributos: Rápido y Confiable.

Rápido: Si bien “rápido” significa diferentes cosas en distintos contextos, voy a explicar los beneficios de velocidad de cargar lo menos posible desde la red.

Confiable: Pero la velocidad bruta no es suficiente. Para que se sienta como una AWP, tu aplicación web debe ser confiable. Debe ser lo suficientemente resiliente como para cargar siempre algo, incluso si solo es una página de error personalizada, sin importar el estado de la red.

Rapidez confiable: Por último, reformularé un poco la definición de la AWP y observaré lo que significa compilar algo que sea confiablemente rápido. No es suficiente ser rápido y confiable solo cuando estás en una red de latencia baja. Ser rápida y confiable significa que la velocidad de la app web es coherente, sin importar las condiciones de la red subyacentes.

Tecnologías de habilitación: Service Workers + API de Cache Storage

Las AWP presentan un alto estándar de velocidad y resiliencia. Afortunadamente, la plataforma web ofrece algunos componentes básicos para hacer realidad ese tipo de rendimiento. Me refiero a los service worker y la API de Cache Storage.

Con la API de Cache Storage, puedes compilar un service worker que escuche las solicitudes entrantes, pase algunas a la red y almacene una copia de la respuesta para usarla en el futuro.

Un service worker que usa la API de Cache Storage para guardar una copia de
          una respuesta de red

La próxima vez que la app web realice la misma solicitud, su service worker podrá verificar sus cachés y solo mostrar la respuesta previamente almacenada en caché.

Un service worker que usa la API de Cache Storage para responder y eludir la red

Evitar la red siempre que sea posible es una parte fundamental para ofrecer un rendimiento rápido y confiable.

JavaScript “isomórfico”

Un concepto más que quiero abordar es lo que a veces se conoce como JavaScript “isomórfico” o “universal”. En pocas palabras, la idea es que el mismo código JavaScript se pueda compartir entre diferentes entornos de ejecución. Cuando compilé mi AWP, quería compartir el código JavaScript entre el servidor de backend y el service worker.

Existen muchos enfoques válidos para compartir código de esta manera, pero mi enfoque consistió en usar módulos de ES como el código fuente definitivo. Luego, transpilamos y empaquetamos esos módulos para el servidor y el service worker con una combinación de Babel y Rollup. En mi proyecto, los archivos con una extensión .mjs son códigos que se encuentran en un módulo ES.

El servidor

Con esos conceptos y términos en mente, profundicemos en cómo compilé mi AWP de Stack Overflow. Primero, veremos nuestro servidor de backend y explicaré cómo encaja en la arquitectura.

Buscaba una combinación de un backend dinámico y hosting estático, y mi enfoque era usar la plataforma de Firebase.

Firebase Cloud Functions iniciará automáticamente un entorno basado en nodos cuando haya una solicitud entrante y se integrará al popular framework de HTTP Express con el que ya estaba familiarizado. También ofrece hosting listo para usar para todos los recursos estáticos de mi sitio. Veamos cómo el servidor maneja las solicitudes.

Cuando un navegador realiza una solicitud de navegación a nuestro servidor, pasa por el siguiente flujo:

Una descripción general de la generación de una respuesta de navegación del servidor.

El servidor enruta la solicitud según la URL y usa la lógica de plantillas para crear un documento HTML completo. Uso una combinación de datos de la API de Stack Exchange y fragmentos HTML parciales que el servidor almacena de forma local. Una vez que el service worker sabe cómo responder, puede comenzar a transmitir HTML a la app web.

Hay dos partes de esta imagen que vale la pena explorar con más detalle: el enrutamiento y las plantillas.

Enrutamiento

En lo que respecta al enrutamiento, mi enfoque fue usar la sintaxis de enrutamiento nativo del framework Express. Es lo suficientemente flexible como para coincidir con los prefijos de URL simples, así como las URLs que incluyen parámetros como parte de la ruta de acceso. Aquí, crearé una asignación entre los nombres de las rutas con los que debe coincidir el patrón Express subyacente.

const routes = new Map([
  ['about', '/about'],
  ['questions', '/questions/:questionId'],
  ['index', '/'],
]);

export default routes;

Luego, puedo hacer referencia a esta asignación directamente desde el código del servidor. Cuando hay una coincidencia para un patrón de Express determinado, el controlador correspondiente responde con una lógica de plantillas específica para la ruta coincidente.

import routes from './lib/routes.mjs';
app.get(routes.get('index'), async (req, res) => {
  // Templating logic.
});

Plantillas del servidor

¿Y cómo es esa lógica de plantillas? Yo elegí un enfoque que reunía fragmentos HTML parciales en secuencia, uno tras otro. Este modelo se presta bien para la transmisión.

El servidor envía código estándar de HTML inicial de inmediato, y el navegador puede procesar esa página parcial de inmediato. A medida que el servidor reúne el resto de las fuentes de datos, las transmite al navegador hasta que el documento está completo.

Para entender lo que quiero decir, observa el código de Express para una de nuestras rutas:

app.get(routes.get('index'), async (req, res) => {
  res.write(headPartial + navbarPartial);
  const tag = req.query.tag || DEFAULT_TAG;
  const data = await requestData(...);
  res.write(templates.index(tag, data.items));
  res.write(footPartial);
  res.end();
});

Mediante el uso del método write() del objeto response y la referencia a plantillas parciales almacenadas de forma local, puedo iniciar la transmisión de respuestas de inmediato, sin bloquear ninguna fuente de datos externa. El navegador toma este HTML inicial y procesa una interfaz significativa y carga el mensaje de inmediato.

La siguiente parte de nuestra página usa datos de la API de Stack Exchange. Obtener esos datos significa que nuestro servidor debe hacer una solicitud de red. La app web no puede renderizar nada más hasta que recibe una respuesta y la procesa, pero al menos los usuarios no miran una pantalla en blanco mientras esperan.

Una vez que la app web recibe la respuesta de la API de Stack Exchange, llama a una función de plantillas personalizada para traducir los datos de la API a su HTML correspondiente.

Lenguaje de plantillas

Las plantillas pueden ser un tema sorprendentemente polémico, y lo que abordé es solo un enfoque entre muchos. Te recomendamos sustituir tu propia solución, en especial si tienes vínculos heredados con un framework de plantillas existente.

Para mi caso de uso, lo que tenía sentido para mi caso de uso era simplemente confiar en los literales de las plantillas de JavaScript, con alguna lógica desglosada en funciones auxiliares. Uno de los beneficios de crear una MPA es que no tienes que realizar un seguimiento de las actualizaciones de estado ni volver a renderizar tu código HTML, por lo que un enfoque básico que produjo el código HTML estático me funcionó.

Este es un ejemplo de cómo plantillas la parte de HTML dinámico del índice de mi aplicación web. Al igual que con mis rutas, la lógica de plantillas se almacena en un módulo ES que se puede importar al servidor y al service worker.

export function index(tag, items) {
  const title = `<h3>Top "${escape(tag)}" Questions</h3>`;
  const form = `<form method="GET">...</form>`;
  const questionCards = items
    .map(item =>
      questionCard({
        id: item.question_id,
        title: item.title,
      })
    )
    .join('');
  const questions = `<div id="questions">${questionCards}</div>`;
  return title + form + questions;
}

Estas funciones de plantilla son JavaScript puro y son útiles para dividir la lógica en funciones auxiliares más pequeñas cuando sea necesario. Aquí, paso cada uno de los elementos que se muestran en la respuesta de la API a una de esas funciones, lo que crea un elemento HTML estándar con todos los atributos adecuados establecidos.

function questionCard({id, title}) {
  return `<a class="card"
             href="/questions/${id}"
             data-cache-url="${questionUrl(id)}">${title}</a>`;
}

Nota: Es un atributo de datos que agrego a cada vínculo, data-cache-url, configurado como la URL de la API de Stack Exchange que necesito para mostrar la pregunta correspondiente. Tenlo en cuenta. Lo volveré a ver más tarde.

Volviendo al controlador de rutas, una vez que se completan las plantillas, transmito la parte final del código HTML de mi página al navegador y, luego, finalizo el flujo. Esta es la señal para el navegador de que se completó la renderización progresiva.

app.get(routes.get('index'), async (req, res) => {
  res.write(headPartial + navbarPartial);
  const tag = req.query.tag || DEFAULT_TAG;
  const data = await requestData(...);
  res.write(templates.index(tag, data.items));
  res.write(footPartial);
  res.end();
});

Este es un breve recorrido por la configuración de mi servidor. Los usuarios que visitan mi app web por primera vez siempre recibirán una respuesta del servidor. Sin embargo, cuando un visitante vuelva a mi app web, el service worker comenzará a responder. Analicémoslo en profundidad.

El service worker

Descripción general de la generación de una respuesta de navegación en el service worker

Este diagrama debería resultarte familiar, ya que muchas de las partes que vimos antes se encuentran aquí con una disposición ligeramente diferente. Analicemos el flujo de la solicitud y tomemos en cuenta el service worker.

Nuestro service worker controla una solicitud de navegación entrante para una URL determinada y, al igual que mi servidor, usa una combinación de lógica de enrutamiento y plantilla para descifrar cómo responder.

El enfoque es el mismo que antes, pero con diferentes primitivas de bajo nivel, como fetch() y la API de Cache Storage. Uso esas fuentes de datos para construir la respuesta HTML, que el service worker envía a la aplicación web.

Workbox

En lugar de comenzar desde cero con primitivas de bajo nivel, voy a compilar mi service worker a partir de un conjunto de bibliotecas de alto nivel llamado Workbox. Proporciona una base sólida para la lógica de generación de respuestas, enrutamiento y almacenamiento en caché de cualquier service worker.

Enrutamiento

Al igual que con mi código del servidor, mi service worker debe saber cómo hacer coincidir una solicitud entrante con la lógica de respuesta adecuada.

Mi enfoque consistía en traducir cada ruta de Express en una expresión regular correspondiente y usar una biblioteca útil llamada regexparam. Una vez realizada la traducción, puedo aprovechar la compatibilidad integrada de Workbox para el enrutamiento de expresiones regulares.

Después de importar el módulo que tiene las expresiones regulares, registro cada expresión regular con el router de Workbox. Dentro de cada ruta, puedo proporcionar lógica de plantillas personalizada para generar una respuesta. Crear plantillas en el service worker es un poco más complejo que en mi servidor de backend, pero Workbox es útil para gran parte del trabajo pesado.

import regExpRoutes from './regexp-routes.mjs';

workbox.routing.registerRoute(
  regExpRoutes.get('index')
  // Templating logic.
);

Almacenamiento en caché de recursos estáticos

Una parte clave del proceso de creación de plantillas es asegurarme de que mis plantillas HTML parciales estén disponibles de forma local a través de la API de Cache Storage y se mantengan actualizadas cuando implemento los cambios en la app web. El mantenimiento de la caché puede ser propenso a errores cuando se realiza manualmente, por lo que hablo con Workbox para manejar el almacenamiento previo en caché como parte de mi proceso de compilación.

Le indica a Workbox qué URLs debe almacenar en caché previamente con un archivo de configuración, que apunta al directorio que contiene todos mis recursos locales junto con un conjunto de patrones que deben coincidir. La CLI de Workbox lee este archivo de forma automática, que se run cada vez que volvo a compilar el sitio.

module.exports = {
  globDirectory: 'build',
  globPatterns: ['**/*.{html,js,svg}'],
  // Other options...
};

Workbox toma una instantánea del contenido de cada archivo y, luego, inserta automáticamente esa lista de URLs y revisiones en el archivo de service worker final. Workbox ahora tiene todo lo que necesita para que los archivos prealmacenados en caché siempre estén disponibles y se mantengan actualizados. El resultado es un archivo service-worker.js que contiene algo similar a lo siguiente:

workbox.precaching.precacheAndRoute([
  {
    url: 'partials/about.html',
    revision: '518747aad9d7e',
  },
  {
    url: 'partials/foot.html',
    revision: '69bf746a9ecc6',
  },
  // etc.
]);

Para quienes usan un proceso de compilación más complejo, Workbox tiene un complemento webpack y un módulo de nodo genérico, además de su interfaz de línea de comandos.

Transmisión

A continuación, quiero que el service worker transmita ese código HTML parcial prealmacenado en caché a la aplicación web de inmediato. Esta es una parte crucial para ser "rápido y confiable", ya que siempre aparece algo significativo en la pantalla de inmediato. Afortunadamente, esto es posible si usas la API de Streams en nuestro service worker.

Es posible que ya hayas oído hablar sobre la API de Streams antes. Mi colega Jake Archhibald lleva años cantando elogios. Hizo la predicción audaz de que 2016 sería el año de los flujos web. La API de Streams es igual de genial hoy que hace dos años, pero con una diferencia crucial.

Si bien en ese momento solo Chrome era compatible con Streams, la API de Streams ahora es más compatible. La historia general es positiva y, con un código de resguardo adecuado, no hay nada que te impida usar transmisiones en tu service worker en la actualidad.

Bueno, puede haber una cosa que te detenga, y es que quieres saber cómo funciona en realidad la API de Streams. Expone un conjunto muy poderoso de primitivas, y los desarrolladores que saben cómo usarla pueden crear flujos de datos complejos, como los siguientes:

const stream = new ReadableStream({
  pull(controller) {
    return sources[0]
      .then(r => r.read())
      .then(result => {
        if (result.done) {
          sources.shift();
          if (sources.length === 0) return controller.close();
          return this.pull(controller);
        } else {
          controller.enqueue(result.value);
        }
      });
  },
});

Sin embargo, comprender las implicaciones completas de este código podría no ser útil para todos. En lugar de analizar esta lógica, hablemos sobre mi enfoque para la transmisión de service worker.

Estoy usando un nuevo wrapper de alto nivel, workbox-streams. Con él, puedo pasarlo a una combinación de fuentes de transmisión, tanto de cachés como de datos del entorno de ejecución que pueden provenir de la red. Workbox se encarga de coordinar las fuentes individuales y unirlas en una sola respuesta de transmisión.

Además, Workbox detecta automáticamente si la API de Streams es compatible y, cuando no lo es, crea una respuesta equivalente sin transmisión. Esto significa que no tienes que preocuparte por escribir resguardos, ya que las transmisiones se acercan al 100% de compatibilidad con navegadores.

Almacenamiento en caché del entorno de ejecución

Veamos cómo mi service worker maneja los datos del entorno de ejecución desde la API de Stack Exchange. Uso la compatibilidad integrada de Workbox para una estrategia de almacenamiento en caché inactiva durante la revalidación, junto con el vencimiento, para garantizar que el almacenamiento de la app web no aumente de forma ilimitada.

Configuré dos estrategias en Workbox para manejar las diferentes fuentes que constituirán la respuesta de la transmisión. En unas pocas llamadas a funciones y configuración, Workbox nos permite hacer lo que, de otro modo, tomaría cientos de líneas de código escrito a mano.

const cacheStrategy = workbox.strategies.cacheFirst({
  cacheName: workbox.core.cacheNames.precache,
});

const apiStrategy = workbox.strategies.staleWhileRevalidate({
  cacheName: API_CACHE_NAME,
  plugins: [new workbox.expiration.Plugin({maxEntries: 50})],
});

La primera estrategia lee datos que se almacenaron previamente en caché, como nuestras plantillas HTML parciales.

La otra estrategia implementa la lógica de almacenamiento en caché de inactividad durante la revalidación, junto con el vencimiento de la caché menos usado recientemente una vez que llegamos a las 50 entradas.

Ahora que puse estas estrategias en marcha, lo único que queda es indicarle a Workbox cómo usarlas para construir una respuesta de transmisión completa. Paso un array de fuentes como funciones, y cada una de esas funciones se ejecutará de inmediato. Workbox toma el resultado de cada fuente y lo transmite a la app web, en secuencia, solo se retrasa si la siguiente función del array aún no se completó.

workbox.streams.strategy([
  () => cacheStrategy.makeRequest({request: '/head.html'}),
  () => cacheStrategy.makeRequest({request: '/navbar.html'}),
  async ({event, url}) => {
    const tag = url.searchParams.get('tag') || DEFAULT_TAG;
    const listResponse = await apiStrategy.makeRequest(...);
    const data = await listResponse.json();
    return templates.index(tag, data.items);
  },
  () => cacheStrategy.makeRequest({request: '/foot.html'}),
]);

Las primeras dos fuentes son plantillas parciales almacenadas previamente en caché que se leen directamente desde la API de Cache Storage, por lo que siempre estarán disponibles de inmediato. Esto garantiza que la implementación de nuestro service worker sea confiable y rápida para responder a las solicitudes, al igual que mi código del servidor.

La siguiente función fuente recupera datos de la API de Stack Exchange y procesa la respuesta en el HTML que espera la app web.

La estrategia de revalidación inactiva significa que, si tengo una respuesta almacenada en caché para esta llamada a la API, podré transmitirla a la página de inmediato mientras actualizo la entrada de caché "en segundo plano" para la próxima vez que se solicite.

Por último, transmito una copia almacenada en caché de mi pie de página y cierro las etiquetas HTML finales para completar la respuesta.

Compartir código mantiene los elementos sincronizados

Verás que algunos fragmentos del código del service worker te resultan familiares. El código HTML parcial y la lógica de plantillas que usa mi service worker son idénticos a los que utiliza mi controlador del servidor. Este uso compartido de código garantiza que los usuarios obtengan una experiencia coherente, ya sea que visiten mi app web por primera vez o regresen a una página renderizada por el service worker. Esa es la belleza del JavaScript isómórfico.

Mejoras progresivas y dinámicas

Revisé el servidor y el service worker de mi AWP, pero hay un último fragmento de lógica por tratar: hay una pequeña cantidad de JavaScript que se ejecuta en cada una de mis páginas, una vez que se transmiten por completo.

Este código mejora la experiencia del usuario de manera progresiva, pero no es crucial, ya que la app web seguirá funcionando si no se ejecuta.

Metadatos de página

Mi app usa JavaScipt del cliente para actualizar los metadatos de una página según la respuesta de la API. Como uso la misma versión inicial de HTML almacenado en caché para cada página, la app web termina con etiquetas genéricas en el encabezado del documento. Sin embargo, a través de la coordinación entre el código de las plantillas y el del cliente, puedo actualizar el título de la ventana con metadatos específicos de la página.

Como parte del código de plantillas, mi enfoque es incluir una etiqueta de secuencia de comandos que contenga la string escapada de forma correcta.

const metadataScript = `<script>
  self._title = '${escape(item.title)}';
</script>`;

Luego, una vez que se cargó mi página, leo la cadena y actualizo el título del documento.

if (self._title) {
  document.title = unescape(self._title);
}

Si hay otras partes de metadatos específicos de la página que deseas actualizar en tu propia aplicación web, puedes seguir el mismo enfoque.

UX sin conexión

La otra mejora progresiva que agregué se utiliza para llamar la atención sobre nuestras capacidades sin conexión. Creé una AWP confiable y quiero que los usuarios sepan que, cuando están sin conexión, aún pueden cargar las páginas que visitaron anteriormente.

Primero, uso la API de Cache Storage para obtener una lista de todas las solicitudes a la API previamente almacenadas en caché y traduzco eso a una lista de URLs.

¿Recuerdas esos atributos de datos especiales que mencioné, cada uno con la URL de la solicitud a la API necesaria para mostrar una pregunta? Puedo comparar esos atributos de datos con la lista de URLs almacenadas en caché y crear un array de todos los vínculos de preguntas que no coinciden.

Cuando el navegador entra en estado sin conexión, realizo un bucle en la lista de vínculos no almacenados en caché y atenúo los que no funcionan. Ten en cuenta que esto es solo una sugerencia visual para el usuario sobre lo que debe esperar de esas páginas. En realidad, no inhabilitaré los vínculos ni evitaré que el usuario navegue.

const apiCache = await caches.open(API_CACHE_NAME);
const cachedRequests = await apiCache.keys();
const cachedUrls = cachedRequests.map(request => request.url);

const cards = document.querySelectorAll('.card');
const uncachedCards = [...cards].filter(card => {
  return !cachedUrls.includes(card.dataset.cacheUrl);
});

const offlineHandler = () => {
  for (const uncachedCard of uncachedCards) {
    uncachedCard.style.opacity = '0.3';
  }
};

const onlineHandler = () => {
  for (const uncachedCard of uncachedCards) {
    uncachedCard.style.opacity = '1.0';
  }
};

window.addEventListener('online', onlineHandler);
window.addEventListener('offline', offlineHandler);

Errores comunes

Ahora repasé mi enfoque para compilar una AWP de varias páginas. Hay muchos factores que deberás considerar cuando elabores tu propio enfoque, y es posible que termines tomando decisiones diferentes a las mías. Esa flexibilidad es una de las grandes cosas de la compilación para la Web.

Hay algunos errores comunes que puedes encontrar cuando tomas tus propias decisiones arquitectónicas, y queremos evitar algunos problemas.

No almacenar HTML completo en caché

Se recomienda no almacenar documentos HTML completos en la caché. Por ejemplo, es una pérdida de espacio. Si tu app web usa la misma estructura HTML básica para cada una de las páginas, terminarás almacenando copias del mismo lenguaje de marcado una y otra vez.

Lo más importante es que, si implementas un cambio en la estructura HTML compartida de tu sitio, cada una de esas páginas previamente almacenadas en caché seguirá teniendo el diseño anterior. Imagina la frustración de un visitante recurrente al ver una combinación de páginas antiguas y nuevas.

Desvío de servidores y service worker

La otra dificultad que debes evitar es que tu servidor y service worker se desincronizan. Mi enfoque fue usar JavaScript isomórfico, de modo que se ejecutara el mismo código en ambos lugares. Según la arquitectura de tu servidor existente, eso no siempre es posible.

Sin importar las decisiones arquitectónicas que tomes, debes tener alguna estrategia para ejecutar el código de enrutamiento y plantillas equivalente en tu servidor y tu service worker.

En el peor de los casos

Disposición incoherente

¿Qué sucede cuando ignoras esos errores? En el peor de los casos, es posible que ocurra todo tipo de fallas, pero un usuario recurrente visita una página almacenada en caché con un diseño muy antiguo, es decir, con un texto de encabezado desactualizado o que usa nombres de clase de CSS que ya no son válidos.

Peor caso: enrutamiento dañado

Como alternativa, un usuario puede encontrar una URL controlada por tu servidor, pero no por tu service worker. Un sitio lleno de diseños zombis y callejones sin salida no es una AWP confiable.

Sugerencias para tener éxito

¡Pero no eres la única persona en esto! Las siguientes sugerencias pueden ayudarte a evitar esas dificultades:

Usa bibliotecas de plantillas y enrutamiento que tengan implementaciones en varios lenguajes

Intenta usar bibliotecas de plantillas y enrutamiento que tengan implementaciones de JavaScript. Sé que no todos los desarrolladores pueden permitirse el lujo de migrar desde tu servidor web actual y usar plantillas de lenguaje.

Sin embargo, varios frameworks populares de plantillas y enrutamiento tienen implementaciones en varios lenguajes. Si puedes encontrar uno que funcione con JavaScript y con el lenguaje de tu servidor actual, estás un paso más cerca de mantener sincronizados tu service worker y el servidor.

Elige plantillas secuenciales en lugar de anidadas

A continuación, te recomiendo usar una serie de plantillas secuenciales que se puedan transmitir una tras otra. No hay problema si las partes posteriores de la página usan una lógica de plantilla más complicada, siempre y cuando puedas transmitir en la parte inicial del código HTML lo más rápido posible.

Almacena en caché el contenido estático y dinámico en tu service worker

Para obtener el mejor rendimiento, debes almacenar previamente en caché todos los recursos estáticos críticos de tu sitio. También debes configurar la lógica de almacenamiento en caché del entorno de ejecución para controlar contenido dinámico, como solicitudes a la API. El uso de Workbox significa que puedes compilar sobre estrategias bien probadas y listas para la producción, en lugar de implementar todo desde cero.

Solo realiza bloqueos en la red cuando sea absolutamente necesario.

En relación con eso, solo debes bloquear en la red cuando no sea posible transmitir una respuesta desde la caché. Mostrar una respuesta de la API almacenada en caché de inmediato, a menudo, puede generar una mejor experiencia del usuario que esperar datos actualizados.

Recursos