Una primera aproximación a los microfrontends

[English version]Este enlace se abrirá en una ventana nueva

Hace unos meses, os hablaba sobre como utilizamos frameworks web modernos dentro de un portalEste enlace se abrirá en una ventana nueva, haciendo hincapié en conceptos tales como la orquestación y la modularización. En este post utilizo las mismas ideas, pero con una aproximación algo distinta. De hecho, me atrevería a decir que de un modo algo más moderno. Una aproximación basada en micro-frontends aunque, a decir, verdad, no es que sea un gran fanático de estos nombres. Si os soy sincero me parece que muchas veces utilizamos nombres nuevos para cosas que se llevaban haciendo hace mucho tiempo. En este caso concreto, un portal podría verse como un orquestador de micro-frontends.

Lo primero que me gustaría explicaros es ¿qué es un micro-frontend? Podría definirlo como una estrategia por la que dividimos el frontend en una serie de fragmentos o elementos que son teóricamente independientes y que, de hecho, se podrían desarrollar de forma también independiente. Aquí si que me parece importante trasladar que existe una diferencia entre lo que hacen los micro-frontends y otras estrategias de división del frontend como puede ser la que se emplea en REACT con su sistema de lazy loading.

Muchos os dirán que la diferencia principal es que los micro-frontends nos proporcionan la indudable ventaja de que podemos desarrollar cada fragmento en una tecnología diferente. Desde mi punto de vista no es tanto eso, sino que los micro-frontends necesitan una orquestación. Sin duda, tendremos lazy loading y podremos desarrollaros con la tecnología que queramos, pero, para mí, lo esencial es que este tipo de aproximación implica dar respuesta a cuándo, cómo y porqué se mostraran estos elementos en la pantalla del usuario.

image00-portadaimage00-portada

Así que, ¿cómo podemos lograr esta orquestación? Desgraciadamente no tenemos una solución global que valga para todo. Lo que me vale a mí, probablemente no valdrá para un caso más general. Pese a ello, si sabéis cual es mi aproximación seguramente podáis entender mejor los retos e incluso proponer soluciones diferentes y mejores que las mías.

Para mí, los aspectos fundamentales de este tipo de arquitectura implican decidir cómo se crean los micro-frontends, cómo se cargan, crear la aplicación principal - el orquestador – y, finalmente, resolver una serie de problemillas que aparecen cuando no tenemos un frontend monolítico. Os lo cuento en los siguientes puntos.

Creando los micro-frontends

Como os decía antes, cada micro-frontend es, en realidad, un elemento independiente. Eso significa que puede tener sus propias dependencias y, de hecho, podríamos incluso pensar que podría ejecutarse de forma independiente. Esto, lógicamente, no debe significar que tengamos una aproximación caótica, sino que necesitamos proporcionar un entorno de trabajo común que haga más fácil el desarrollo de los servicios.

En nuestro caso estamos utilizando diferentes técnicas. Entre ellas, tenemos una carpeta que utilizamos como si fuera un Marketplace de los servicios de la aplicación; utilizamos yarn y sus workspaces para evitar tener que descargar medio Internet para cada servicio que creemos; un generador de servicios que nos facilita la creación de esqueletos y un empaquetador personalizado que no sólo se orienta a transpilar el código, sino a preparar el micro-frontend para ser utilizado desde el motor de orquestación.

imagen04-gradleimagen04-gradle

Cargando los micro-frontend

A decir verdad, cuando me refiero a "cargar" los micro-frontend no me estoy refiriendo, realmente, a mostrarlos en pantalla. Sino a ser consciente de su existencia dentro del ámbito de nuestra aplicación y a proporcionar mecanismos que permitan la extracción del código y estilos desde el orquestador de frontend.

Para mí, esta tarea no es realmente de frontend sino de backend. Es decir, pongamonos en la tesitura de querer hacer una aplicación modular, ¿qué quiero hacer? Pues, básicamente, poder añadir nuevos módulos y añadirlos sin tener que tocar el código del orquestador de frontend. Así pues parece que lo único que podemos hacer es que el conocimiento de lo que hay descanse en una parte servidora sea esta un backend ad-hoc, una base de datos o una aproximación serverless.

¿Cómo resolverlo? A través de una aproximación múltiple y complementaria. En concreto, utilizamos un watchdog para detectar cambios en la estructura de carpetas, lo que nos permite detectar nuevos servicios o servicios modificados; un mecanismo de análisis de la estructura del servicio y despliegue en Base de datos; un mecanismo que permite su descarga desde el orquestador de frontend y una capa de seguridad que se encarga de garantizar que el código sólo puede ser descargado por usuarios autorizados.

imagen01-watchdogimagen01-watchdog

Lógicamente, necesitaríamos exponer el código interno del micro-frontend, más bien un punto de entrada, de tal forma que pueda ser invocado desde el orquestador. Para ello, podemos aprovecharnos en las capacidades de webpack que nos permiten publicar el código transpilado como una librería.

imagen05-webpackimagen05-webpack

El orquestador

Construir una solución completa es complejo. Imaginaos que queréis configurar, con una aproximación WYSIWYG. dónde se van a mostrar cada uno de vuestros elementos en pantalla, o si tiene que haber diferentes layouts para cada una de las posibles rutas de vuestra aplicación. Si vuestro caso de uso es ese, yo os recomendaría iros a una solución de Portal.

Pero en este caso concreto, buscaba una solución más simple. Sólo un menú que sirviera como puerta de acceso a los diferentes micro-frontends afectados. De esta forma con almacenar el orden de los elementos en el menú sería suficiente y eso, se puede hacer dentro de las fronteras del micro-frontend sin tener que complicarnos excesivamente la vida.

imagen03-imagen03-

Lógicamente sería necesario implementar la aproximación de lazy-loading, este aspecto es relativamente sencillo y es una tarea bastante común en JavaScript

imagen06-lazyloadimagen06-lazyload

En este caso, lo que quería era ir un paso más allá. Así que decidí que porque no hacer un wrap de toda la carga de estilos y código JavaScript en un componente WebEste enlace se abrirá en una ventana nueva. Esta aproximación es bastante interesante, puesto que nos permite hacer uso del Shadow DOM y que todo sea más directo desde el punto de vista del orquestador de micro-frontends.

imagen02-webcomponent00imagen02-webcomponent00

La verdad es que cuando utilizáis el Shadow DOM podéis tener problemas con el uso de estilos, tendréis que tomar decisiones. Además de que igual os vais a encontrar con alguna cosilla que no os gusta si vuestros micro-frontends se empeñan en abrir ventanas de diálogo. Si no sois cuidadosos el diálogo podría mostrarse en el global DOM en lugar de en el shadow aplicando los estilos que no querríais que se aplicaran. Creo que merece la pena probarlo, pero yo, personalmente, prefiero para mi caso de uso no hacer un shadowing del micro-frontend.

Aspectos a tener en cuenta

Más allá de lo comentado creo que hay otros aspectos que conviene tener en cuenta cuando tomamos una aproximación como la que os he comentado. A mi modo de ver los más relevantes serían:

  • Acceso a red. Cuando tenemos distintos servicios que se renderizan de forma independiente al motor de renderizado principal, tenemos un problemilla ni tenemos contextos ni nada por el estilo. Esto que parece menor, puede suponer un problema importante cuando tenemos que considerar, por ejemplo, aspectos como logout global de la aplicación ante errores de caducidad de sesión. La solución podría pasar por hacer uso de variables globales, aunque creo que aumenta el acoplamiento y, por tanto, no es que me guste mucho. Otra opción sería, por ejemplo, modificar el objeto fetch global para que haga algo diferente.

imagen07-fetchimagen07-fetch

  • Problemas de enrutamiento. Parece una tontería, pero vais a tener dos sistemas de enrutamiento independientes que tienen que coexistir, el del orquestador y el del micro-frontend. Mi aproximación utilizar un motor de routing que permita mantener un enrutamiento global para el orquestador y otro basado en hash para el micro-frontend.
  • Exponer librerías compartidas a todos los micro-frontends. Ponte en la situación de que todos tus micro-frontends están desarrollados con React, así que, podrías preguntarte, ¿por qué no exponer globalmente react, react-dom o redux? De esta forma podríamos minimizar el tamaño del código empaquetado. Pero ¡cuidado! Si tomas esta aproximación deberías garantizar la consistencia de versiones entre el orquestador y los micro-frontends a fin de que no pasen cosas raras. Así que, como regla general, no lo recomendaría.
  • Compartir información entre micro-frontends una parte compleja puesto que implica tomar decisiones sobre el cómo. Aunque no lo he tratado en el post, podrían emplearse técnicas como eventos o incluso almacenando algo en el IndexedDB local (cuidado con la información sensible), etc.