Growing Pains: implementando Micro-Frontends para agilizar nuestro desarrollo Web
Tras haber lanzado la versión móvil de nuestra Web a inicios de 2019, nos encontrábamos en una situación similar a nuestras apps móviles — teníamos un monolito — y con él, debíamos buscar cómo resolver los siguientes desafíos:
- Permitir a los equipos realizar más releases y de manera independiente: Con la arquitectura existente, los cambios debían ser validados por un equipo responsable de la estabilidad de la web y, en caso de encontrarse algún bug, esto demoraba la salida a producción del resto de los equipos.
- Mantener la calidad: teniendo múltiples equipos trabajando en múltiples cambios sobre el mismo código, las posibilidades de introducir bugs aumentaba considerablemente.
- Dar a los equipos capacidad de innovar en distintas tecnologías: al tratarse de un monolito, introducir un nuevo componente al stack implicaba una situación de “todo o nada”, sin margen de experimentación.
Dado que planificábamos duplicar el tamaño de los equipos de Frontend a nivel compañía durante 2020, un cambio de arquitectura era mandatorio para poder escalar correctamente.
Cambio de Arquitectura
En nuestra búsqueda por atacar los desafíos mencionados, encontramos que la arquitectura basada en Micro Frontends tenía el potencial de resolverlos.
Esta arquitectura posee un enfoque similar a microservicios, es decir, permite que cada equipo trabaje de manera independiente y pueda deployar a producción tantas veces como sea necesario.
A su vez, cada Micro Frontend reside en un repositorio separado lo que permite una clara separación de responsabilidades para evitar pisarse entre equipos y a la vez medir correctamente la calidad del código.
Para lograr esto, es necesario partir el monolito en subpartes (micro frontend) y luego interconectarlas para que el usuario final utilice la aplicación como un todo.
La idea detrás de Micro Frontends
Cada Micro Frontend, observado en la sección de “Recursos Disponibles” a la izquierda del diagrama posee una responsabilidad. Cada equipo es dueño de uno y maneja a su disposición su ciclo de desarrollo.
A la derecha tenemos una aplicación, que se construye utilizando los recursos disponibles como si fueran bloques de construcción.
Cada Micro Frontend, se registra en la aplicación bajo ciertas condiciones del objeto “location” es decir el path. En el ejemplo podemos observar que podemos llamar a “New App” si accedemos al recurso raíz “/”, mientras que si el usuario navega a “/cart”, accede a un Micro Frontend distinto.
A su vez podemos observar tres Micro Frontends: Logger, Tracker y FWF. Estos son Micro Frontends base y construidos para que sean reutilizables y así evitar repetir lógica común entre funcionalidades.
Pasaje de datos entre Micro Frontends
Teniendo los micrositios aislados, existen distintas estrategias para compartir datos entre ellos y así poder realizar los distintos flujos de la aplicación.
Los posibles escenarios son:
- Navegar de un Micro Frontend al otro (Micro A → Micro B): En este caso pasamos lo mínimo e indispensable via query string dado que el receptor debería poder generar todo el contexto necesario comunicándose con el backend. También es posible compartir la sesión del usuario, pero rara vez es utilizado.
- Comunicación entre dos Micro Frontends activos: En este caso, dado que no hay navegación entre ellos podemos utilizar Callback Functions o Custom Events de Javascript.
- Navegación desde una App Nativa a un Micro Frontend: Es similar al primer caso pero sumamos encabezados HTTP para la autenticación y autorización del Request realizado desde nuestras apps.
- Navegación de un Micro Frontend a una App Nativa: En este último caso podemos recurrir a Callback Functions en caso de estar ambas activas en una misma pantalla o a Deeplinks en caso que necesitemos navegar fuera del Micro Frontend a otra pantalla nativa.
Separando el estado global
Otro desafío interesante fue cómo refactorizar el manejo del estado global de la aplicación, dado que violaba el principio de “no compartir nada” entre Micro Frontends para evitar el acoplamiento.
A la izquierda del diagrama podemos observar a los Módulos A y B por ejemplo teniendo su propio segmento dentro del estado global, compartiendo el mismo con otros módulos (Módulo C) y por lo tanto evitando ir al backend para consultar esos datos.
Claro está, esto tiene como consecuencia que los módulos deben conocerse entre ellos y saber qué datos consumen, generando una gran dependencia.
Es por esto, que nos movimos al esquema que se ve a la derecha del diagrama, donde cada módulo tiene una interfaz que provee un contrato indicando los parámetros necesarios para construir su contexto. Si bien esto implica ir más veces al backend, tiene la gran ventaja de desacoplar los Micro Frontends y ganar independencia para los equipos.
El framework elegido: Single-SPA
Para implementar la arquitectura nos decidimos por Single-SPA por las siguientes razones:
- Soporte a migración desde React SPA Monolítica a Micro Frontends
- Proyecto Open Source con una gran comunidad activa
- Implementado con Vanilla Javascript
- Permite coexistir diferentes frameworks (React, Vue, Angular, etc)
Migrando a la nueva arquitectura
El desafío más importante al que nos enfrentábamos era cómo migrar a la nueva arquitectura sin tener que reescribir todo el código de nuestra web.
Aquí es donde Single-SPA sobresale como solución dado que provee un método para exportar nuestro código actual como un objeto que implementa una pequeña interfaz (single-spa-react) a la que luego se inicializa (bootstrap) y se monta o desmonta (mount/unmount) cada vez que se la necesite.
import React from ‘react’;
import ReactDOM from ‘react-dom’;
import rootComponent from ‘./path-to-root-component.js’;
import singleSpaReact, {SingleSpaContext} from ‘single-spa-react’;const reactLifecycles = singleSpaReact({
React,
ReactDOM,
rootComponent,
errorBoundary(err, info, props) {
return (
<div>This renders when a catastrophic error occurs</div>
);
},
});export const bootstrap = reactLifecycles.bootstrap;
export const mount = reactLifecycles.mount;
export const unmount = reactLifecycles.unmount;
Debido a que este proyecto no podía ser realizado en uno o dos sprints, debimos trabajar en ramas separadas, debiendo mantener sincronizadas nuestra Web con arquitectura React SPA y la nueva como Micro Frontend.
A su vez, usamos versionamiento semántico para proveer claridad sobre qué versión estábamos trabajando, siendo 2.x.x la versión actual y 3.x.x la que implementaba la nueva arquitectura.
Despliegue a producción
Nuestra estrategia de despliegue fue por país, aumentando el porcentaje de tráfico semana a semana, midiendo cómo performaba la nueva arquitectura y permitiéndonos tener control sobre cualquier inconveniente que pudiera aparecer.
A su vez creamos reglas por encabezados HTTP que nos permitieran forzar consumir la arquitectura nueva para poder realizar pruebas y validar que todo estuviera funcionando correctamente en producción.
En paralelo, los equipos comenzaron a crear distintos Micro Frontends para desacoplarse del monolito y salir a producción una vez finalizado el despliegue inicial.
Aprendizajes
Para finalizar este post, nos gustaría resumir los aprendizajes y puntos a favor y en contra en caso que estés pensando en adoptar una arquitectura similar.
Como principales puntos a favor destacamos la autonomía de los equipos donde dejan de depender de terceros para pasar a ser dueños completamente de su código, la calidad del mismo y despliegues a producción.
Esto, sumado a la posibilidad de utilizar y experimentar con diferentes frameworks de JS, permite a los equipos poner el foco en la construcción de nuevas funcionalidades sin tener que verse atados por la tecnología que lo sustenta.
Otra ventaja destacada es la posibilidad de utilizar una funcionalidad tanto en web como en una app nativa mediante integraciones vía Webviews, debido a su naturaleza desacoplada.
Es importante mencionar que una solución con esta arquitectura aumenta la complejidad de monitoreo global de la salud de la aplicación así como también requiere especial tratamiento para el testeo end to end, tema que abordaremos en futuros posts. Esto es fácilmente mitigable con una buena herramienta de monitoreo como Datadog, New Relic o Sentry.
Resumiendo, el enfoque de micro-frontends nos permitió dar un salto en independencia de equipos, velocidad de deployment y abrir la posibilidad de innovar en diferentes tecnologías sin tener conflictos con soluciones actuales. Aún con esta nueva arquitectura, estamos siempre en búsqueda de mejores implementaciones que superen a esta y nos permitan aún mayores beneficios. Nos encantaría saber qué mejoras o diferentes propuestas has enfrentado en tus proyectos o si tenés alguna duda sobre la nuestra. ¡Te leemos!
Esta nota fue realizara en colaboración junto con Leonel Corso y el equipo