Suscríbete para recibir notificaciones de nuevas publicaciones:

Adopción incremental de micro-frontends con Cloudflare Workers

2022-11-17

10 min de lectura
Esta publicación también está disponible en English, 繁體中文, Français, Deutsch, 日本語, Português y 简体中文.

Añade los beneficios del micro-frontend a las aplicaciones web heredadas

Incremental adoption of micro-frontends with Cloudflare Workers

Recientemente, escribimos sobre una nueva arquitectura basada en fragmentos para crear aplicaciones web. Esta solución es rápida, rentable y se adapta a los proyectos más grandes. También permite un ciclo de iteración rápido. El enfoque utiliza varios Cloudflare Workers que colaboran para representar y transmitir micro-frontends en una aplicación que es interactiva de forma más rápida que en los enfoques tradicionales del lado del cliente. Esto permite mejorar la experiencia del usuario y las puntuaciones de SEO.

Este enfoque es excelente si estás comenzando un nuevo proyecto o tienes la capacidad de volver a escribir tu aplicación actual desde cero. Sin embargo, en realidad, la mayoría de los proyectos son demasiado grandes para reconstruirlos desde cero y solo pueden adoptar cambios arquitectónicos de manera incremental.

En esta publicación, proponemos una forma de reemplazar solo partes seleccionadas de una aplicación representada heredada del lado del cliente con fragmentos representados del lado del servidor. El resultado es una aplicación en la que las vistas más importantes son interactivas antes, se pueden desarrollar de forma independiente y obtienen todos los beneficios del enfoque de micro-frontend. Al mismo tiempo, evita tener que reescribir a gran escala el código base heredado. Este enfoque es independiente del marco. En esta publicación demostramos fragmentos creados con React, Qwik y SolidJS.

El problema de las grandes aplicaciones frontend

Muchas aplicaciones frontend grandes desarrolladas hoy en día no logran ofrecer una buena experiencia de usuario. Esto se debe, a menudo, a arquitecturas que requieren la descarga, el análisis y la ejecución de grandes cantidades de código JavaScript antes de que los usuarios puedan interactuar con la aplicación. A pesar de los esfuerzos por diferir el código JavaScript no crítico a través de la carga diferida y el uso de la representación del lado del servidor, estas aplicaciones grandes aún tardan demasiado en ser interactivas y responder a las entradas que realiza el usuario.

Además, el desarrollo y la implementación de las aplicaciones monolíticas grandes pueden ser complejos. Varios equipos pueden estar colaborando en un código base único y el esfuerzo de coordinar las pruebas y la implementación del proyecto dificulta el desarrollo, la implementación y la iteración de funciones individuales.

Como describimos en nuestra publicación anterior, los micro-frontends basados en Cloudflare Workers pueden resolver estos problemas, pero convertir una aplicación monolítica en una arquitectura de micro-frontend puede ser difícil y costoso. Es posible que se requieran meses, o incluso años, de desarrollo de ingeniería hasta que los usuarios o los desarrolladores perciban las ventajas.

Lo que necesitamos es un enfoque en el que un proyecto pueda adoptar gradualmente micro-frontends en las partes más importantes de la aplicación, sin tener que volver a escribir toda la aplicación de una sola vez.

Fragmentos al rescate

El objetivo de una arquitectura basada en fragmentos es reducir significativamente la carga y la latencia de la interacción en el caso de las aplicaciones web grandes (según las mediciones realizadas por Core Web Vitals). Para ello, dividimos la aplicación en micro-frontends que se pueden representar (y almacenar en caché) rápidamente en Cloudflare Workers. El desafío es cómo integrar un fragmento de micro-frontend en una aplicación representada del lado del cliente heredada de manera que tenga un coste mínimo para el proyecto original.

La técnica que proponemos nos permite convertir las partes más importantes de la interfaz de usuario de una aplicación heredada, de manera aislada del resto de la aplicación.

Resulta que, en muchas aplicaciones, las partes más importantes de la interfaz de usuario a menudo están anidadas en un "shell" de aplicación que proporciona elementos de encabezado, pie de página y navegación. Son, por ejemplo, un formulario de inicio de sesión, un panel de detalles del producto en una aplicación de comercio electrónico, la bandeja de entrada en un cliente de correo electrónico, etc.

Veamos un formulario de inicio de sesión como ejemplo. Si nuestra aplicación tarda varios segundos en mostrar el formulario de inicio de sesión, los usuarios se mostrarán reticentes a iniciar sesión y es posible que los perdamos. Sin embargo, podemos convertir el formulario de inicio de sesión en un fragmento representado del lado del servidor, que se muestra e interactúa de inmediato, mientras el resto de la aplicación heredada se inicia en segundo plano. Dado que el fragmento es interactivo desde el principio, el usuario puede incluso enviar sus credenciales antes de que se inicie la aplicación heredada.

Animación que muestra el formulario de inicio de sesión disponible antes de la aplicación principal

Animation showing the login form being available before the main application

Este enfoque permite a los equipos de ingeniería ofrecer mejoras importantes a los usuarios en tan solo una fracción del tiempo y el coste de ingeniería en comparación con los enfoques tradicionales, los cuales sacrifican las mejoras en la experiencia del usuario o requieren una reescritura larga y arriesgada de toda la aplicación. Permite a los equipos con aplicaciones monolíticas de una sola página adoptar una arquitectura de micro-frontend de forma incremental, orientar las mejoras a las partes más importantes de la aplicación y, por lo tanto, anticipar el retorno de la inversión.

Un desafío interesante de la extracción de partes de la interfaz de usuario en fragmentos representados del lado del servidor es que, una vez que se muestran en el navegador, queremos que la aplicación heredada y los fragmentos se sientan como una sola aplicación. Los fragmentos deben estar perfectamente integrados en el shell de la aplicación heredada, manteniendo la aplicación accesible al formar correctamente la jerarquía del código DOM. Sin embargo, también queremos que los fragmentos representados del lado del servidor se muestren y se vuelvan interactivos lo antes posible, incluso antes de que aparezca el shell de la aplicación representada heredada del lado del cliente. ¿Cómo podemos incorporar fragmentos de la interfaz de usuario en un shell de aplicación que aún no existe? Para resolver este problema, ideamos una técnica a la que llamamos "inserción de fragmentos".

Inserción de fragmentos

La inserción de fragmentos combina código HTML/DOM generado por fragmentos de micro-frontend representados del lado del servidor con código HTML/DOM generado por una aplicación representada heredada del lado del cliente.

Los fragmentos de micro-frontend se representan directamente en el nivel superior de la respuesta HTML y están diseñados para ser interactivos de inmediato. En segundo plano, la aplicación heredada se representa en el lado del cliente como un hermano de estos fragmentos. Cuando está listo, los fragmentos se "insertan" en la aplicación heredada (el código DOM de cada fragmento se mueve a su ubicación correspondiente en el código DOM de la aplicación heredada) sin causar efectos secundarios visuales o pérdida del estado del lado del cliente, como el enfoque, los datos del formulario o la selección de texto. Una vez "insertado", un fragmento puede comenzar a comunicarse con la aplicación heredada y se convierte de hecho en una parte integrada de esta.

Aquí, puedes ver un fragmento de "inicio de sesión" y el elemento "raíz" vacío de la aplicación heredada en el nivel superior del código DOM, antes de la inserción.

<body>
  <div id="root"></div>
  <piercing-fragment-host fragment-id="login">
    <login q:container...>...</login>
  </piercing-fragment-host>
</body>

Y aquí puedes ver que el fragmento se ha insertado en el div "login-page" en la aplicación representada heredada.

<body>
  <div id="root">
    <header>...</header>
    <main>
      <div class="login-page">
        <piercing-fragment-outlet fragment-id="login">
          <piercing-fragment-host fragment-id="login">
            <login  q:container...>...</login>
          </piercing-fragment-host>
        </piercing-fragment-outlet>
      </div>
    </main>
    <footer>...</footer>
  </div>
</body>

Para evitar que el fragmento se mueva y provoque un cambio de diseño visible durante esta transición, aplicamos estilos CSS que posicionan el fragmento de la misma manera antes y después de la inserción.

En cualquier momento, una aplicación puede mostrar cualquier número de fragmentos insertados o ninguno. Esta técnica no se limita solo a la carga inicial de la aplicación heredada. Los fragmentos también pueden añadirse y eliminarse en cualquier momento en una aplicación. Esto permite que se representen en respuesta a las interacciones del usuario y al enrutamiento del lado del cliente.

Con la inserción de fragmentos, puedes comenzar a adoptar micro-frontends de manera incremental, un fragmento cada vez. Tú decides la granularidad de los fragmentos y qué partes de la aplicación convertir en fragmentos. No es necesario que todos los fragmentos usen el mismo marco web, lo que puede ser útil al cambiar de pila o durante una integración de varias aplicaciones posterior a la adquisición.

La demostración "Productivity Suite"

Como demostración de la inserción de fragmentos y la adopción incremental, hemos desarrollado una aplicación de demostración “Productivity Suite” que permite a los usuarios gestionar listas de tareas pendientes, leer noticias acerca de hackers, etc. Implementamos el shell de esta aplicación como una aplicación React representada del lado del cliente (una opción tecnológica habitual en las aplicaciones corporativas). Esta es nuestra "aplicación heredada". Hay tres rutas en la aplicación que se han actualizado para usar fragmentos de micro-frontends:

  • /login: un formulario de inicio de sesión ficticio simple con validación del lado del cliente, que se muestra cuando los usuarios no están autenticados (implementado en Qwik).

  • /todos: gestiona una o más listas de tareas pendientes, implementadas como dos fragmentos de colaboración:

    • Selector de listas de tareas pendientes: un componente para seleccionar/crear/eliminar listas de tareas pendientes (implementado en Qwik).

    • Editor de listas de tareas pendientes: un clon de la aplicación TodoMVC (implementado en React).

  • /news: un clon de la demostración HackerNews (implementado en SolidJS).

Esta demostración muestra que se pueden usar diferentes tecnologías independientes, tanto para la aplicación heredada como para cada uno de los fragmentos.

Una visualización de los fragmentos que se insertan en la aplicación heredada

La aplicación se implementa en https://productivity-suite.web-experiments.workers.dev/.

Para probarla, primero debes iniciar sesión. Simplemente utiliza el nombre de usuario que desees (no se necesita ninguna contraseña). Los datos del usuario se guardan en una cookie, por lo que puedes cerrar la sesión y volver a iniciarla con el mismo nombre de usuario. Una vez que hayas iniciado sesión, desplázate por las distintas páginas, usando la barra de navegación de la parte superior de la aplicación. En concreto, echa un vistazo a las páginas “Todo Lists” y “News” para ver la inserción en acción.

En cualquier momento, intenta volver a cargar la página para ver que los fragmentos se procesan al instante mientras la aplicación heredada se carga lentamente en segundo plano. ¡Intenta interactuar con los fragmentos incluso antes de que aparezca la aplicación heredada!

En la parte superior de la página hay controles que te permiten ver el impacto de la inserción de fragmentos en acción.

  • Utiliza el control deslizante "Legacy app bootstrap delay" para establecer el retardo simulado antes de que inicie la aplicación heredada.

  • Alterna "Piercing Enabled" para ver cuál sería la experiencia del usuario si la aplicación no usara fragmentos.

  • Alterna "Show Seams" para ver dónde se encuentra cada fragmento en la página actual.

Cómo funciona

La aplicación está compuesta por una serie de bloques de creación.

Una descripción general de Workers colaborativo y el servidor de la aplicación heredada

El servidor de la aplicación heredada en nuestra demostración sirve los archivos que definen la aplicación React del lado del cliente (HTML, JavaScript y hojas de estilo). Las aplicaciones creadas con otras pilas tecnológicas también funcionarían correctamente. Los Fragment Workers alojan los fragmentos de micro-frontend, como se describe en nuestra publicación anterior acerca de la arquitectura basada en fragmentos. El Gateway Worker maneja las solicitudes del navegador, seleccionando, obteniendo y combinando flujos de respuesta de la aplicación heredada y fragmentos de micro-frontend.

Una vez que todas estas partes se han implementado, funcionan conjuntamente para manejar cada solicitud del navegador. Veamos qué sucede si vas a la ruta "/login".

El flujo de solicitudes al visualizar la página de inicio de sesión

El usuario navega a la aplicación y el navegador realiza una solicitud al Gateway Worker para obtener el código HTML inicial. El Gateway Worker identifica que el navegador está solicitando la página de inicio de sesión. A continuación, realiza dos subsolicitudes paralelas: una para obtener el index.html de la aplicación heredada y otra para solicitar el fragmento de inicio de sesión representado en el lado del servidor. A continuación, combina estas dos respuestas en un solo flujo de respuesta que contiene el código HTML que se envía al navegador.

El navegador muestra la respuesta HTML que contiene el elemento raíz vacío para la aplicación heredada y el fragmento de inicio de sesión representado en el lado del servidor, que es interactivo para el usuario de manera inmediata.

A continuación, el navegador solicita el código JavaScript de la aplicación heredada. El Gateway Worker envía esta solicitud al servidor de la aplicación heredada. De la misma forma, cualquier otro recurso para la aplicación heredada o los fragmentos se enruta a través del Gateway Worker al servidor de la aplicación heredada o al Fragment Worker correspondiente.

Una vez que se ha descargado y ejecutado el código JavaScript de la aplicación heredada, representando el shell de la aplicación en el proceso, se inicia la inserción del fragmento, moviendo el fragmento a la ubicación correspondiente de la aplicación heredada, mientras se conserva todo su estado de la interfaz de usuario.

Aunque nos centramos en el fragmento de inicio de sesión para explicar la inserción del fragmento, las mismas ideas se aplican a los otros fragmentos implementados en las rutas /todos y /news.

La biblioteca de inserción

Aunque los fragmentos se implementan utilizando distintos marcos web, todos se integran en la aplicación heredada de la misma manera, utilizando ayudantes de una “Piercing Library”. Esta biblioteca es una colección de utilidades del lado del servidor y del lado del cliente que desarrollamos para la demostración, a fin de manejar la integración de la aplicación heredada con fragmentos de micro-frontend. Las características principales de la biblioteca son la clase PiercingGateway, los elementos personalizados fragment host y fragment outlet y la clase MessageBus.

PiercingGateway

La clase PiercingGateway se puede usar para crear instancias de un Gateway Worker que maneja todas las solicitudes de código HTML, JavaScript y otros recursos de nuestra aplicación. El "PiercingGateway" enruta las solicitudes a través de los Fragment Workers correspondientes o al servidor de la aplicación heredada. También combina los flujos de respuesta HTML de estos fragmentos con la respuesta de la aplicación heredada en un solo flujo HTML que se devuelve al navegador.

La implementación de un Gateway Worker es sencilla utilizando la biblioteca de inserción. Crea una nueva instancia de puerta de enlace de PiercingGateway, pasándole la URL al servidor de la aplicación heredada y una función para determinar si la inserción está activada para la solicitud especificada. Exporta la puerta de enlace como la exportación predeterminada desde el script de Worker para que el tiempo de ejecución de Workers pueda conectar tu controlador fetch().

const gateway = new PiercingGateway<Env>({
  // Configure the origin URL for the legacy application.
  getLegacyAppBaseUrl: (env) => env.APP_BASE_URL,
  shouldPiercingBeEnabled: (request) => ...,
});
...

export default gateway;

Los fragmentos se pueden registrar llamando al método registerFragment() para que la puerta de enlace pueda enrutar automáticamente las solicitudes del código HTML y los recursos de un fragmento a su Fragment Worker. Por ejemplo, el registro del fragmento de inicio de sesión tendría este aspecto:

gateway.registerFragment({
  fragmentId: "login",
  prePiercingStyles: "...",
  shouldBeIncluded: async (request) => !(await isUserAuthenticated(request)),
});

Servidor y salida de fragmentos

El enrutamiento de las solicitudes y la combinación de las respuestas HTML en Gateway Worker es solamente la mitad de lo que hace posible la inserción. La otra mitad debe ocurrir en el navegador, donde los fragmentos se deben insertar en la aplicación heredada mediante la técnica que hemos descrito anteriormente.

La inserción de fragmentos en el navegador se realiza gracias a un par de elementos personalizados, el servidor de fragme<piercing-fragment-host>) y la salida de fragmentos (<piercing-fragment-outlet>).

Gateway Worker envuelve el código HTML de cada fragmento en un servidor de fragmentos. En el navegador, el servidor de fragmentos administra la vida útil del fragmento y se utiliza al mover el código DOM del fragmento a su posición en la aplicación heredada.

<piercing-fragment-host fragment-id="login">
  <login q:container...>...</login>
</piercing-fragment-host>

En la aplicación heredada, el desarrollador marca dónde debe aparecer un fragmento al insertarlo añadiendo una salida de fragmento. La ruta de inicio de sesión de nuestra aplicación de demostración tiene el aspecto siguiente:

export function Login() {
  …
  return (
    <div className="login-page" ref={ref}>
      <piercing-fragment-outlet fragment-id="login" />
    </div>
  );
}

Cuando se añade una salida de fragmentos al código DOM, este busca en el documento actual su servidor de fragmentos asociado. Si lo encuentra, el servidor de fragmentos y su contenido se mueven a la salida. Si no lo encuentra, la salida hará una solicitud al Worker de la puerta de enlace para obtener el código HTML del fragmento, que a continuación se transmite directamente a la salida del fragmento, utilizando la biblioteca writable-dom (una biblioteca pequeña, pero eficaz, desarrollada por el equipo de MarkoJS).

Este mecanismo alternativo permite la navegación del lado del cliente a rutas que contienen nuevos fragmentos. De esta forma, los fragmentos se pueden representar en el navegador a través de la navegación inicial (dura) y la navegación del lado del cliente (suave).

Bus de mensajes

A menos que los fragmentos de nuestra aplicación sean completamente de presentación o autónomos, también se deben comunicar con la aplicación heredada y otros fragmentos. El [MessageBus](https://github.com/cloudflare/workers-web-experiments/blob/df50b60cfff7bc299cf70ecfe8f7826ec9313b84/productivity-suite/piercing-library/src/message-bus/message-bus.ts#L18) es un bus de comunicación simple, asíncrono, isomorfo e independiente del marco al que pueden acceder la aplicación heredada y cada uno de los fragmentos.

En nuestra aplicación de demostración, el fragmento de inicio de sesión debe informar a la aplicación heredada cuando el usuario se haya autenticado. Este envío de mensajes se implementa en el componente Qwik LoginForm de la siguiente forma:

const dispatchLoginEvent = $(() => {
  getBus(ref.value).dispatch("login", {
    username: state.username,
    password: state.password,
  });
  state.loading = true;
});

A continuación, la aplicación heredada puede escuchar estos mensajes, de esta forma:

useEffect(() => {
  return getBus().listen<LoginMessage>("login", async (user) => {
    setUser(user);
    await addUserDataIfMissing(user.username);
    await saveCurrentUser(user.username);
    getBus().dispatch("authentication", user);
    navigate("/", { replace: true, });
  });
}, []);

Optamos por esta implementación de bus de mensajes porque necesitábamos una solución que fuera independiente del marco y que funcionara correctamente tanto en el servidor como en el cliente.

¡Pruébalo!

Con los fragmentos, la inserción de fragmentos y Cloudflare Workers, puedes mejorar el rendimiento y el ciclo de desarrollo de las aplicaciones representadas heredadas del lado del cliente. Estos cambios los puedes adoptar de forma gradual e incluso mientras implementas fragmentos con un marco web de elijas.

La aplicación "Productivity Suite" que demuestra estas capacidades se puede encontrar en https://productivity-suite.web-experiments.workers.dev/.

Todo el código que hemos mostrado aquí es de código abierto y está publicado en Github: https://github.com/cloudflare/workers-web-experiments/tree/main/productivity-suite.

No dudes en clonar el repositorio. Es fácil ejecutarlo a nivel local e incluso implementar tu propia versión (de forma gratuita) en Cloudflare. Hemos intentado que el código sea lo más reutilizable posible. La mayor parte de la lógica principal se encuentra en la biblioteca de inserción, que puedes probar en tus propios proyectos. Nos encantaría recibir tus comentarios, sugerencias o que nos cuentes para qué aplicaciones te gustaría utilizarlo. Únete a nuestro debate en GitHub o también puedes ponerte en contacto con nosotros en nuestro canal Discord.

Estamos convencidos de que la combinación de Cloudflare Workers con las últimas ideas de los marcos impulsará los próximos avances importantes para mejorar la experiencia tanto para los usuarios como para los desarrolladores en las aplicaciones web. Encontrarás más demostraciones, publicaciones y colaboraciones a medida que continuamos ampliando los límites de lo que la web puede ofrecer. Si, además, deseas participar directamente en este proceso, nos complace compartir que buscamos nuevos empleados.

Protegemos redes corporativas completas, ayudamos a los clientes a desarrollar aplicaciones web de forma eficiente, aceleramos cualquier sitio o aplicación web, prevenimos contra los ataques DDoS, mantenemos a raya a los hackers, y podemos ayudarte en tu recorrido hacia la seguridad Zero Trust.

Visita 1.1.1.1 desde cualquier dispositivo para empezar a usar nuestra aplicación gratuita y beneficiarte de una navegación más rápida y segura.

Para saber más sobre nuestra misión para ayudar a mejorar Internet, empieza aquí. Si estás buscando un nuevo rumbo profesional, consulta nuestras ofertas de empleo.
Developer Week (ES)DesarrolladoresCloudflare WorkersEdgeMicro-frontendsDeveloper Platform

Síguenos en X

Peter Bacon Darwin|@petebd
Igor Minar|@IgorMinar
Cloudflare|@cloudflare

Publicaciones relacionadas