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

¿Hablamos de… arquitectura?

Abordaré un tema importante, pero que puede malinterpretarse: la arquitectura que usas para tu app web y, específicamente, cómo entran en juego tus decisiones de arquitectura cuando creas una app web progresiva.

La "arquitectura" puede sonar vaga, y es posible que no quede claro de inmediato por qué es importante. Una forma de pensar en la arquitectura es hacerte las siguientes preguntas: Cuando un usuario visita una página de mi sitio, ¿qué HTML se carga? Y, 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. Por lo tanto, mi objetivo es guiarte por una posible arquitectura que me pareció eficaz. A lo largo de 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 encajan todas las piezas te inspire y que te sientas con la capacidad de personalizar esto para que se adapte a tus necesidades.

PWA de Stack Overflow

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

Apps de varias páginas (MPA)

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

MPA es un nombre elegante para la arquitectura tradicional que se usa desde el comienzo de la Web. Cada vez que un usuario navega a una URL nueva, el navegador renderiza de forma progresiva el código HTML específico de esa página. No se intenta conservar el estado de la página ni el contenido entre las navegaciones. Cada vez que visitas una página nueva, comienzas de cero.

Esto contrasta con el modelo de app de una sola página (SPA) para compilar apps web, en el que el navegador ejecuta 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 igualmente válidos para usar, pero, en esta publicación, quería explorar los conceptos de las AWP en el contexto de una app de varias páginas.

Velocidad confiable

Me has oído (y a muchas otras personas) usar la frase "app web progresiva" o AWP. Es posible que ya conozcas parte del material de referencia, en otra parte de este sitio.

Puedes pensar en una AWP como una app web que proporciona una experiencia del usuario de primer nivel y que realmente se gana un lugar en la pantalla principal del usuario. La sigla "FIRE", que significa Fast (rápida), Integrated (integrada), Reliable (confiable) y Engaging (atractiva), resume todos los atributos que se deben tener en cuenta cuando se crea una AWP.

En este artículo, me enfocaré en un subconjunto de esos atributos: Rápido y Confiable.

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

Confiable: Sin embargo, la velocidad bruta no es suficiente. Para que se sienta como una AWP, tu app web debe ser confiable. Debe ser lo suficientemente resiliente como para cargar siempre algo, incluso si es solo una página de error personalizada, independientemente del estado de la red.

Confiablemente rápida: Por último, reformularé un poco la definición de PWA y analizaré qué significa compilar algo que sea confiablemente rápido. No basta con ser rápido y confiable solo cuando estás en una red de baja latencia. Ser confiablemente rápida significa que la velocidad de tu app web es constante, independientemente de las condiciones de red subyacentes.

Tecnologías habilitadoras: Service Workers y API de Cache Storage

Las PWA establecen un estándar alto de velocidad y resiliencia. Afortunadamente, la plataforma web ofrece algunos componentes básicos para que ese tipo de rendimiento sea una realidad. Me refiero a los service workers y a 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, a través de la API de Cache Storage.

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 devolver la respuesta almacenada en caché anteriormente.

Un service worker que usa la API de Cache Storage para responder, omitiendo 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 denomina JavaScript "isomórfico" o "universal". En pocas palabras, es la idea de que el mismo código de JavaScript se puede compartir entre diferentes entornos de ejecución. Cuando creé mi PWA, quería compartir código de JavaScript entre mi servidor de backend y el service worker.

Hay muchos enfoques válidos para compartir código de esta manera, pero mi enfoque fue usar módulos de ES como el código fuente definitivo. Luego, transpilé y empaqueté 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 de archivo .mjs son código que se encuentra en un módulo de ES.

El servidor

Con esos conceptos y terminología en mente, veamos cómo creé mi PWA de Stack Overflow. Comenzaré por explicar nuestro servidor de backend y cómo se integra en la arquitectura general.

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

Firebase Cloud Functions activará automáticamente un entorno basado en Node cuando haya una solicitud entrante y se integrará con el 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 controla las solicitudes.

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

Una descripción general de cómo generar una respuesta de navegación del lado 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 nuestro service worker sepa cómo responder, podrá comenzar a transmitir HTML de vuelta a nuestra app web.

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

Enrutamiento

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

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 adecuado 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 se ve esa lógica de plantillas? Bueno, elegí un enfoque que unía fragmentos parciales de HTML en secuencia, uno tras otro. Este modelo se adapta bien a la transmisión.

El servidor devuelve de inmediato un código HTML estándar inicial, y el navegador puede renderizar 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 se completa el documento.

Para ver a qué nos referimos, consulta el código de Express de 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();
});

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

La siguiente parte de nuestra página usa datos de la API de Stack Exchange. Para obtener esos datos, nuestro servidor debe realizar una solicitud de red. La app web no puede renderizar nada más hasta que reciba una respuesta y la procese, 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 plantilla personalizada para traducir los datos de la API a su HTML correspondiente.

Lenguaje de plantillas

La creación de plantillas puede ser un tema sorprendentemente polémico, y lo que elegí 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.

Lo que tenía sentido para mi caso de uso era confiar en los literales de plantilla de JavaScript, con cierta lógica dividida en funciones auxiliares. Uno de los aspectos positivos de crear una MPA es que no tienes que hacer un seguimiento de las actualizaciones de estado ni volver a renderizar tu HTML, por lo que un enfoque básico que produjera HTML estático funcionó para mí.

Este es un ejemplo de cómo creo la plantilla de la parte dinámica de HTML del índice de mi app web. Al igual que con mis rutas, la lógica de plantillas se almacena en un módulo de ES que se puede importar tanto al servidor como 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 es útil dividir la lógica en funciones auxiliares más pequeñas cuando sea apropiado. Aquí, paso cada uno de los elementos que se muestran en la respuesta de la API a una de esas funciones, 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>`;
}

Es de especial importancia un atributo de datos que agrego a cada vínculo, data-cache-url, configurado en la URL de la API de Stack Exchange que necesito para mostrar la pregunta correspondiente. Tenlo en cuenta. Volveré a revisarlo más adelante.

Volviendo a mi controlador de rutas, una vez que se completa la creación de plantillas, transmito la parte final del HTML de mi página al navegador y finalizo la transmisión. Esta es la indicación 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();
});

Ese fue un breve recorrido por la configuración de mi servidor. Los usuarios que visiten mi app web por primera vez siempre recibirán una respuesta del servidor, pero cuando un visitante regrese a mi app web, mi service worker comenzará a responder. Analicemos este tema.

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: muchas de las mismas piezas que ya mencioné están aquí en una disposición un poco diferente. Veamos el flujo de solicitudes, teniendo 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 plantillas para determinar cómo responder.

El enfoque es el mismo que antes, pero con diferentes primitivas de bajo nivel, como fetch() y la API de Storage de caché. Uso esas fuentes de datos para construir la respuesta HTML, que el service worker pasa de vuelta a la app web.

Workbox

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

Enrutamiento

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

Mi enfoque fue traducir cada ruta de Express en una expresión regular correspondiente, con una biblioteca útil llamada regexparam. Una vez que se realiza esa traducción, puedo aprovechar la compatibilidad integrada de Workbox con 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. La creación de plantillas en el service worker es un poco más compleja que en mi servidor de backend, pero Workbox ayuda con 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 de la historia de las plantillas es asegurarse de que mis plantillas HTML parciales estén disponibles de forma local a través de la API de Storage de caché y se mantengan actualizadas cuando implemente cambios en la app web. El mantenimiento de la caché puede ser propenso a errores cuando se realiza de forma manual, por lo que recurro a Workbox para controlar el almacenamiento previo en caché como parte de mi proceso de compilación.

Le indico a Workbox qué URLs debe almacenar en caché previa 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 automáticamente este archivo, que se ejecuta cada vez que vuelvo 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 mi archivo final de service worker. Workbox ahora tiene todo lo que necesita para que los archivos almacenados en caché previa estén siempre disponibles y 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 de webpack y un módulo de nodo genérico, además de su interfaz de línea de comandos.

Transmisión

Luego, quiero que el service worker transmita ese HTML parcial almacenado en caché de vuelta a la app web de inmediato. Esta es una parte crucial de ser "confiablemente rápido": siempre obtengo algo significativo en la pantalla de inmediato. Por suerte, usar la API de Streams dentro de nuestro service worker lo hace posible.

Es posible que ya hayas oído hablar de la API de Streams. Mi colega Jake Archibald lo ha estado elogiando durante años. Hizo la predicción audaz de que el 2016 sería el año de los flujos web. Y la API de Streams es tan increíble hoy como lo era hace dos años, pero con una diferencia crucial.

Si bien en ese momento solo Chrome admitía Streams, la API de Streams ahora tiene una compatibilidad más amplia. La historia general es positiva y, con el código de resguardo adecuado, no hay nada que te impida usar transmisiones en tu service worker hoy mismo.

Bueno, tal vez haya algo que te detenga, y es entender cómo funciona realmente la API de Streams. Expone un conjunto muy potente de primitivas, y los desarrolladores que se sienten cómodos usándolo pueden crear flujos de datos complejos, como el siguiente:

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, es posible que no todos comprendan las implicaciones completas de este código. En lugar de analizar esta lógica, hablemos de mi enfoque para la transmisión de service workers.

Estoy usando un wrapper de alto nivel nuevo, workbox-streams. Con él, puedo pasarlo en una combinación de fuentes de transmisión, tanto de cachés como de datos de tiempo de ejecución que podrían 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 se admite la API de Streams y, cuando no es así, crea una respuesta equivalente que no es de transmisión. Esto significa que no tienes que preocuparte por escribir resguardos, ya que las transmisiones se acercan al 100% de compatibilidad con los navegadores.

Almacenamiento en caché durante el tiempo de ejecución

Veamos cómo mi service worker maneja los datos del entorno de ejecución, de la API de Stack Exchange. Estoy usando la compatibilidad integrada de Workbox con una estrategia de almacenamiento en caché stale-while-revalidate, junto con el vencimiento para garantizar que el almacenamiento de la app web no crezca sin límites.

Configuré dos estrategias en Workbox para controlar las diferentes fuentes que conformarán la respuesta de transmisión. Con algunas llamadas a funciones y configuraciones, Workbox nos permite hacer lo que, de otro modo, requerirí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 los datos que se almacenaron en caché previamente, como nuestras plantillas HTML parciales.

La otra estrategia implementa la lógica de almacenamiento en caché stale-while-revalidate, junto con el vencimiento de la caché menos usada recientemente una vez que alcanzamos las 50 entradas.

Ahora que tengo esas estrategias implementadas, solo queda 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, y 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 en caché previamente que se leen directamente desde la API de Storage de caché, por lo que siempre estarán disponibles de inmediato. Esto garantiza que nuestra implementación del service worker sea rápida y confiable para responder a las solicitudes, al igual que mi código del servidor.

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

La estrategia stale-while-revalidate significa que, si tengo una respuesta almacenada en caché anteriormente 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 todo sincronizado

Notarás que algunas partes del código del service worker te resultan familiares. La lógica de plantillas y el HTML parcial que usa mi service worker son idénticos a los que usa 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 que regresen a una página renderizada por el service worker. Esa es la belleza del JavaScript isomórfico.

Mejoras dinámicas y progresivas

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

Este código mejora progresivamente la experiencia del usuario, pero no es crucial: la app web seguirá funcionando si no se ejecuta.

Metadatos de página

Mi app usa JavaScript del cliente para actualizar los metadatos de una página según la respuesta de la API. Como uso el mismo fragmento inicial de HTML almacenado en caché para cada página, la app web termina con etiquetas genéricas en el encabezado de mi documento. Sin embargo, a través de la coordinación entre mi código de 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 plantilla, mi enfoque es incluir una etiqueta de secuencia de comandos que contenga la cadena con escape correcto.

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

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

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

Si hay otros metadatos específicos de la página que quieras actualizar en tu propia app web, puedes seguir el mismo enfoque.

UX sin conexión

La otra mejora progresiva que agregué se usa para llamar la atención sobre nuestras capacidades sin conexión. Creé una PWA confiable y quiero que los usuarios sepan que, cuando no tienen conexió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 almacenadas en caché anteriormente y la traduzco en una lista de URLs.

¿Recuerdas esos atributos de datos especiales de los que hablé, cada uno con la URL de la solicitud a la API necesaria para mostrar una pregunta? Puedo comparar datos mediante referencia cruzada 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 un estado sin conexión, recorro la lista de vínculos no almacenados en caché y atenúo los que no funcionarán. Ten en cuenta que esto es solo una sugerencia visual para el usuario sobre lo que debe esperar de esas páginas. No estoy inhabilitando los vínculos ni impidiendo 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

Ya te mostré mi enfoque para compilar una PWA de varias páginas. Hay muchos factores que deberás tener en cuenta cuando diseñes tu propio enfoque, y es posible que termines tomando decisiones diferentes a las mías. Esa flexibilidad es uno de los aspectos más importantes de la compilación para la Web.

Existen algunos errores comunes que puedes cometer cuando tomas tus propias decisiones de arquitectura, y quiero evitarte algunos dolores de cabeza.

No almacenes en caché el HTML completo

No te recomiendo almacenar documentos HTML completos en la caché. Por un lado, es un desperdicio de espacio. Si tu app web usa la misma estructura HTML básica para cada una de sus páginas, terminarás almacenando copias del mismo lenguaje de marcado una y otra vez.

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

Desviación del servidor o del service worker

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

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

Peores situaciones posibles

Diseño o disposición inconsistente

¿Qué sucede cuando ignoras esas dificultades? Bueno, es posible que se produzcan todo tipo de fallas, pero el peor de los casos es que un usuario recurrente visite una página almacenada en caché con un diseño muy obsoleto, tal vez una con texto de encabezado desactualizado o que use nombres de clases de CSS que ya no son válidos.

Peor caso: Enrutamiento roto

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

Sugerencias para tener éxito

Pero no estás solo en esto. Las siguientes sugerencias pueden ayudarte a evitar esos problemas:

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

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

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

Prefiere las plantillas secuenciales en lugar de las 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 tu página usan una lógica de plantillas más complicada, siempre y cuando puedas transmitir la parte inicial de tu 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 en caché previa todos los recursos estáticos esenciales de tu sitio. También debes configurar la lógica de almacenamiento en caché del entorno de ejecución para controlar el contenido dinámico, como las solicitudes a la API. Usar Workbox significa que puedes desarrollar sobre estrategias bien probadas y listas para producción en lugar de implementar todo desde cero.

Solo bloquea en la red cuando sea absolutamente necesario

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

Recursos