Este artículo muestra un enfoque para utilizar diferentes frameworks de Javascript y versiones juntos dejando claro los pros y los contras.

Traducción en español del artículo original de Manfred Steyer Multi-Framework and -Version Micro Frontends with Module Federation: Your 4 Steps Guide actualizado el 02.05.2022

La mayoría de los artículos sobre Module Federation asumen que sólo tienes una versión de tu principal Framework, por ejemplo, Angular. Sin embargo, ¿qué hacer si tienes que mezclar diferentes versiones o diferentes frameworks? No te preocupes, te tenemos cubierto. Este artículo utiliza un ejemplo para explicar cómo desarrollar tal escenario en 4 pasos.

Aquí tienes la demo y el código fuente

¿Patrón o anti-patrón?

Mi amigo Luca Mezzalira  en su reciente charla sobre Micro Frontend Anti Patterns, menciona el uso de varios frameworks de frontend en una aplicación. Él llama a este anti patrón la Hidra de Lerna. Este nombre viene de un monstruo acuático de la mitología griega y romana que tiene varias cabezas.

Hay una buena razón para considerar esto un anti patrón: Los frameworks actuales no están preparados para ser arrancados en la misma pestaña del navegador junto con otros frameworks u otras versiones de ellos mismos. Además de que genera applicationes más grandes, esto también aumenta la complejidad y requiere algunas soluciones.

Si tu quieres ampliar el contenido pasa por Danywalls.com

Sin embargo, Luca también explica que hay algunas situaciones en las que este enfoque puede ser necesario. Y cita los siguientes ejemplos:

  1. Tratar con sistemas heredados
  2. Migración a un nuevo marco de trabajo o biblioteca de interfaz de usuario
  3. Mezclar diferentes empresas con diferente stack tecnológico

Siendo sincero esto cuenta mi "historia"  y repito en muchas  conferencias y en los talleres de nuestra empresa: Por favor, intenta evitar mezclar frameworks y versiones en el navegador. Sin embargo, si tienes una buena razón para hacerlo después de descartar las alternativas, hay maneras de hacer que funcionen los Micro Frontends Multi-Framework/Multi-Version.

Como siempre en el área de la arquitectura de software - y probablemente en la vida en general - se trata de trade-offs. Así que si descubres que este enfoque tiene menos inconvenientes que las alternativas con respecto a tus propios objetivos de arquitectura, ve a por ello.

¿Micro Frontends como Componentes Web?

Aunque no es 100% necesario, puede ser una buena idea envolver tus Micro Frontends en Componentes Web.

Esto trae varias ventajas:

  • Abstracción de las diferencias entre frameworks
  • importar o montar o eliminar un Web Component es facil.
  • Shadow DOM ayuda a aislar los estilos CSS
  • Los eventos y propiedades personalizadas permiten comunicarse que se comuniquen.

Las dos primeras opciones se correlacionan entre sí. Necesitamos mostrar y ocultar nuestros Micro Frontends bajo demanda, por ejemplo, al activar un elemento de menú específico. Como cada Micro Frontend es un frontend autónomo, esto también significa que tenemos que arrancarlo bajo demanda en medio de nuestra página. Para ello, los diferentes frameworks proporcionan diferentes métodos o funciones. Cuando se envuelve en Componentes Web, todo lo que tenemos que hacer es añadir o eliminar el elemento HTML respectivo registrado con el Componente Web.

Aislar los estilos CSS mediante Shadow DOM ayuda a que los equipos sean más autosuficientes. Sin embargo, he visto que muy a menudo los equipos cambian un poco de independencia por algunas reglas CSS globales proporcionadas por el shell. En este caso, la emulación de Shadow DOM proporcionada por Angular (con y sin Web Components) es una buena opción: Mientras que evita que los estilos de otros componentes se mezclen con los tuyos, también permite compartir estilos globales.

Además, los eventos y propiedades personalizados parecen ser una buena opción para comunicarse a primera vista. Sin embargo, en aras de la simplicidad, mientras tanto, prefiero un objeto simple que actúe como mediador o "mini bus de mensajes" en el namespace global.

En general, tenemos que ver que tales Componentes Web que envuelven Micro Frontends enteros no son Componentes Web típicos. Hago hincapié en esto porque a veces la gente confunde la idea de un Componente (Web) con la idea de un Micro Frontend o utiliza estos términos como sinónimos. Esto lleva a que los Micro Frontends sean demasiado finos y causen muchos problemas de integración.

¿Necesitamos también Module Federation?

Module Federation facilita la carga de partes de otras aplicaciones en un host. En nuestro caso, el host es el shell del Micro Frontend. Además, Module Federation permite compartir bibliotecas entre el shell y los micro frontends.

Incluso viene con varias estrategias para tratar con diferentes de versiones. Por ejemplo, podemos configurarlo para reutilizar una biblioteca existente si las versiones coinciden exactamente. En caso contrario, podemos indicarle que cargue la versión con la que se ha construido.

La carga de los Micro Frontends discutidos con Module Federation nos da lo mejor de ambos mundos. Podemos compartir bibliotecas cuando sea posible y cargar las nuestras cuando no:

Los 4 pasos

Ahora, después de discutir la estrategia de implementación, veamos los 4 pasos prometidos para construir tal solución.

Paso 1: Envuelve tu Micro Frontend en un Componente Web

Para envolver los Micro Frontends basados en Angular en un Componente Web, puedes ir con Angular Elements proporcionado por el equipo de Angular. Instálalo a través de npm:

npm i @angular/elements

Después de instalarlo, ajusta tu AppModule como sigue:

import { createCustomElement } from '@angular/elements';
 [...]
 
 @NgModule({
   [...]
   declarations: [
     AppComponent
   ],
   bootstrap: [] // No bootstrap components!
 })
 export class AppModule implements DoBoostrap {
   constructor(private injector: Injector) {
   }
 
   ngDoBootstrap() {
     const ce = createCustomElement(AppComponent, {injector: this.injector});
     customElements.define('angular1-element', ce);
   }
 
 }

Esto hace varias cosas:

  • Al ir con un array bootstrap vacío, Angular no arrancará directamente ningún componente al iniciar. Sin embargo, en estos casos, Angular nos exige colocar una lógica de bootstrap personalizada en el método ngDoBootstrap descrito por la interfaz DoBoostrap.
  • ngDoBootstrap utiliza createCustomElement de Angular Elements para envolver tu AppComponent en un Web Component. Para que funcione con DI, también necesitas pasar el Injector actual.
  • El método customElements.define registra el Web Component bajo el nombre angular1-elemento con el navegador.

El resultado de esto es que el navegador montará la aplicación en cada etiqueta angular1-elemento que ocurra en tu aplicación.

Si tu framework no soporta directamente componentes web, también puedes envolver a mano tu aplicación. Por ejemplo, un componente React podría ser envuelto de la siguiente manera:

// app.js
     import React from 'react'
     import ReactDOM from 'react-dom'

     class App extends React.Component {

       render() {
         const reactVersion = require('./package.json').dependencies['react'];

         return ([
             <h1>
               React
             </h1>,
             <p>
               Versión de React: {reactVersion}
             </p>
         ])
       }
     }

     class Mfe4Element extends HTMLElement {
       connectedCallback() {
         ReactDOM.render(<App/>, this);
       }
     }

     customElements.define('react-element', Mfe4Element);

Paso 2: Exponer el componente web a través de Module Federation

Para poder cargar los Micro Frontends en el shell, necesitamos exponer los Web Components que los envuelven a través de Module Federation. Para ello, añade el paquete @angular-architects/module-federation a tu Micro Frontend basado en Angular:

ng add @angular-architects/module-federation

Esto instala e inicializa el paquete. Si usas Nx y Angular, lo más habitual es hacer ambos pasos por separado:

npm i @angular-architects/module-federation -D
     ng g @angular-architects/module-federation:init

En el caso de otros frameworks como React o Vue, todo esto es simplemente añadir el ModuleFederationPlugin a la configuración de webpack. Recuerda que en la mayoría de los casos necesitas arrancar tu aplicación de forma asíncrona. Por lo tanto, tu archivo de entrada contendrá más o menos un import dinámico cargando el resto de la aplicación.

Por esta razón, el Micro Frontend basado en React que hemos comentado anteriormente utiliza el siguiente index.js como punto de entrada:

// index.js
     import('./app');

Del mismo modo, @angular-architects/module-federation traslada el código bootstrap de main.ts a un recién creado bootstrap.ts y lo importa:

// main.ts
     import('./bootstrap');

Este patrón común da a Module Federation el tiempo necesario para cargar las dependencias compartidas.

Después de configurar Module Federation, exponga wrapper basada en Web Component a través de la configuración de webpack:

// webpack.config.js
 [...]
 module.exports = {
   [...]
   plugins: [
     new ModuleFederationPlugin({
 
       name: "angular1",
       filename: "remoteEntry.js",
 
       exposes: {
         './web-components': './src/bootstrap.ts',
       },
 
       shared: share({
         "@angular/core": { requiredVersion: "auto" },
         "@angular/common": { requiredVersion: "auto" },
         "@angular/router": { requiredVersion: "auto" },
         "rxjs": { requiredVersion: "auto" },
 
         ...sharedMappings.getDescriptors()
       }),
       [...]
     })
   ],
 };

Como el objetivo es mostrar cómo mezclar diferentes versiones de Angular, este Micro Frontend utiliza Angular 12 mientras que el shell mostrado a continuación utiliza una versión de Angular más reciente. Por lo tanto, también se utiliza una versión más antigua de @angular-architects/module-federation y la configuración original es más verbosa.  Mas detalles sobre las diferentes versiones.

La configuración en la sección shared asegura que podemos mezclar varias versiones de un framework pero también reutilizar uno ya cargado si los números de versión encajan exactamente. Para ello, requiredVersion debe apuntar a la versión instalada - la que también se encuentra en su package.json. El método de ayuda share que viene con @angular-architects/module-federation se encarga de esto al establecer requiredVersion a auto.

Mientras que según el versionado semántico una librería de Angular con una versión menor o de parche superior es compatible hacia atrás, no existen tales garantías para el código ya compilado. La razón es que el código emitido por el compilador de Angular utiliza las APIs internas de Angular para las que no se aplica la semántica. Por lo tanto, debes usar un número de versión exacto (sin ningún ^ o ~).

Paso 3: Angular y las diferentesd versiones

Para hacer que varias aplicaciones de Angular funcionen juntas en una ventana del navegador, necesitamos algunas soluciones. Lo bueno es que las hemos implementado una herramienta  @angular-architects/module-federation llamado @angular-architects/module-federation-tools.

Sólo tienes que instalarlo (npm i @angular-architects/module-federation-tools -D) en tanto en tu Micro Frontends como en tu shell. Entonces, arranca tu shell y tus Micro Frontends con su método bootstrap en lugar de con el de Angular:

// main.ts
import { AppModule } from './app/app.module';
import { environment } from './environments/environment';
import { bootstrap } from '@angular-architects/module-federation-tools';

bootstrap(AppModule, {
  production: environment.production,
  appType: 'microfrontend'  // for micro frontend
  // appType: 'shell',      // for shell
});

Paso 4: Cargar Micro Frontends en el Shell

También, toca habilitar Module Federation en su shell. Si es un shell basado en Angular, añade el plugin @angular-architects/module-federation:

ng add @angular-architects/module-federation

Como se ha mencionado anteriormente, en el caso de Nx y Angular, se realiza la instalación e inicialización por separado:

npm i @angular-architects/module-federation -D
    ng g @angular-architects/module-federation:init --type host

El flag -type host genera una configuración típica de host. Está disponible desde la versión 14.3 del plugin y, por tanto, desde Angular 14.

Para este ejemplo, no necesitamos ajustar el webpack.config.js generado:

// webpack.config.js
    const { shareAll, withModuleFederationPlugin } = require('@angular-architects/module-federation/webpack');

    module.exports = withModuleFederationPlugin({

        shared: {
            ...shareAll({ singleton: true, strictVersion: true, requiredVersion: 'auto' }),
        },

    });

Los otros ajustes proporcionados por el ModuleFederationPlugin no son necesarios aquí.

Después de esto, todo lo que necesitas es usar lazy loading, para cargar los Micro Frontends:

import { WebComponentWrapper, WebComponentWrapperOptions } from '@angular-architects/module-federation-tools';

    export const APP_ROUTES: Rutas = [
        [...]
        {
            ruta: 'react',
            componente: WebComponentWrapper,
            datos: {
                remoteEntry: 'https://witty-wave-0a695f710.azurestaticapps.net/remoteEntry.js',
                remoteName: 'react',
                exposedModule: './web-components',

                elementName: 'react-elemento'
            } como WebComponentWrapperOptions
        },
        [...]
    ]

El WebComponentWrapper utilizado aquí es proporcionado por @angular-architects/module-federation-tools. Simplemente carga el Web Component a través de Module Federation utilizando los datos clave dados. En el caso mostrado, esta aplicación react se despliega como una Azure Static Web App. Los valores para remoteName y exposedModule se encuentran en la configuración de webpack del Micro Frontend.

El componente wrapper también crea un elemento HTML con el nombre react-element en el que se monta el Web Component.

Si cargas un Micro Frontend compilado con Angular 13 o superior, debes establecer la propiedad type a module:

export const APP_ROUTES: Rutas = [
        [...]
        {
            ruta: 'angular1',
            componente: WebComponentWrapper,
            datos: {
              tipo: 'módulo',
              remoteEntry: 'https://your-path/remoteEntry.js',
              exposedModule: './web-components',

              elementName: 'angular1-elemento'
            } como WebComponentWrapperOptions
        },
        [...]
    }

Además, en el caso de Angular 13+ no necesitas la propiedad remoteName. La razón de estas dos diferencias es que Angular CLI 13+ ya no emite archivos "old-style JavaScript" sino módulos JavaScript. Su manejo en Module Federation es un poco diferente.

Si tu Micro Frontend trae su propio router, necesitas decirle a tu shell que el Micro Frontend añadirá más segmentos a la URL. Para ello, puedes utilizar el comparador startsWith también proporcionado por @angular-architects/module-federation-tools:

importar {
        startsWith,
        WebComponentWrapper,
        WebComponentWrapperOptions
    }
    from '@angular-architects/module-federation-tools';

    [...]

    export const APP_ROUTES: Rutas = [
        [...]
        {
            matcher: startsWith('angular3'),
            componente: WebComponentWrapper,
            datos: {
                [...]
            } as WebComponentWrapperOptions
        },
        [...]
    }

Para que esto funcione, el prefijo de ruta angular3 utilizado aquí tiene que ser utilizado también por el Micro Frontend. Como la configuración del enrutamiento es sólo una estructura de datos, encontrarás formas de añadirla dinámicamente.

Resultado

El resultado de este esfuerzo es una aplicación que consiste en diferentes frameworks de versiones respectivas:

Siempre que sea posible, el marco se comparte. En caso contrario, se carga un nuevo framework ( y versión) por Module Federation. Otra ventaja de este enfoque es que funciona sin ningún meta framework adicional. Sólo se necesitan algunas funciones de ayuda.

Las desventajas son el aumento de la complejidad y el tamaño de los paquetes. Además, nos salimos del camino de los casos de uso soportados: Ninguno de los frameworks ha sido probado oficialmente junto con otros frameworks u otras versiones de sí mismo en la misma pestaña del navegador.

¿Qué es lo siguiente?

Hasta ahora, hemos visto cómo descomponer una gran aplicacion en varios Micro Frontends que pueden incluso utilizar diferentes frameworks. Sin embargo, cuando se trata de frontends de escala empresarial, surgen varias preguntas adicionales:

  • ¿Según qué criterios podemos subdividir una aplicación enorme en subdominios?
  • ¿Cómo podemos imponer el acoplamiento flexible?
  • ¿Cómo podemos asegurarnos de que la solución se puede mantener durante años o incluso décadas?
  • ¿Qué patrones probados deberíamos utilizar?
  • ¿Qué otras opciones de Micro Frontends son proporcionadas por Module Federation?
  • ¿Debemos ir con un monorepo o con varios?

Esas preguntas la responde en el libro de Enterprise Angular DDD, Nx Monorepos y Micro Frontends (en ingles)

Plataforma de cursos gratis sobre programación