Importante: Este artículo está escrito para Angular y Angular CLI 14.

En el artículo anterior, he mostrado cómo usar Module Federation, este es parte de webpack a partir de la versión 5, para implementar micro frontends. Este artículo muestra cómo crear un micro frontend basado en Angular utilizando el router para cargar un micro frontend compilado y desplegado por separado.

Antes de continuar avanzando, recuerda que si tu quieres aprender acerca de Micro-Frontend tenemos 🥳 WORKSHOP

El resultado es similar al del artículo anterior, pero usando Angular:

El Micro frontend cargado se muestra dentro del borde discontinuo rojo sin la shell:

Puede descargar el código desde Github

Usando Module Federation  en proyectos de Angular

Nuestro ejemplo aquí asume que tanto el shell como el micro frontend son proyectos en el mismo espacio de trabajo de Angular. Para empezar, necesitamos decirle a la CLI que use Module Federation al momento de generarlos. Sin embargo, como la CLI nos simplifica la configuracion de webpack, nosotros necesitamos hacer en webpack de forma personalizada.

Para eso el paquete @angular-architects/module-federation proporciona esa interfaz media para interactuar y modificar el mismo. Para empezar, utilizarlo y agregarlo en el proyecto, podemos usar el comando ng add :

ng add @angular-architects/module-federation --project shell --port 4200 --type host
ng add @angular-architects/module-federation --project mfe1 --port 4201 --type remote

Si usas Nx, debes instalar la librería por separado. Después de eso, puede utilizar el flag :init

npm i @angular-architects/module-federation -D 
ng g @angular-architects/module-federation:init --project shell --port 4200 --type host
ng g @angular-architects/module-federation:init --project mfe1 --port 4201 --type remote
El argumento  --type fue añadido en la versión 14.3 y asegura que sólo se genere la configuración necesaria.

Aunque es obvio que el shell del proyecto contiene el código del shell, mfe1 significa Micro Frontend 1,  el comando mostrado hace varias cosas:

  • Generar una base de un webpack.config.js para usar Module Federation
  • Instalar un constructor personalizado haciendo que webpack dentro del CLI utilice el webpack.config.js generado.
  • Asigna un nuevo puerto para ng serve para que varios proyectos puedan ser servidos simultáneamente.

Ten en cuenta que el webpack.config.js es sólo una configuración parcial de webpack. Sólo contiene cosas para controlar module federation. El resto es generado por el CLI como siempre.

El Shell (Host)

Empecemos con el shell, que también se llama host en module federation, este utiliza el router para cargar de forma asyncronza el FlightModule:

export const APP_ROUTES: Routes = [
    {
      path: '',
      component: HomeComponent,
      pathMatch: 'full'
    },
    {
      path: 'flights',
      loadChildren: () => import('mfe1/Module').then(m => m.FlightsModule)
    },
];

Sin embargo, la ruta mfe1/Module que se importa aquí, no existe dentro del shell. Es sólo una ruta virtual que apunta a otro proyecto.

Para facilitar el compilador de TypeScript, necesitamos una definicion .d.ts para ello:

// decl.d.ts
declare module 'mfe1/Module';

Además, tenemos que decirle a webpack que todas las rutas que empiezan por mfe1 apuntan a otro proyecto. Esto se puede hacer en el webpack.config.js generado:

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

module.exports = withModuleFederationPlugin({

  remotes: {
    "mfe1": "http://localhost:4201/remoteEntry.js",
  },

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

});

La sección de remote asigna la ruta mfe1 al micro frontend a su entrada remota. Este es un pequeño archivo generado por webpack cuando se construye el remote, este es cargado en tiempo de ejecución para obtener toda la información necesaria para interactuar con el micro frontend.

Aunque especificar la URL de la entrada remota de esta manera es conveniente para el desarrollo, necesitamos un enfoque más dinámico para la producción.

El siguiente artículo de esta serie brindaremos una solución para esto: Dynamic Federation

La propiedad shared define los paquetes npm a compartir entre el shell y el/los micro frontend(s). Para esta propiedad, la configuración utiliza la propiedad shareAll que básicamente comparte todas las dependencias encontradas en su package.json. Si bien esto ayuda a obtener rápidamente una configuración , podría conducir a demasiadas dependencias compartidas.

Mas adelante hablaremos sobre este tema.

La combinación de singleton: true y strictVersion: true hace que webpack emita un error de ejecución cuando el shell y los micro frontend(s) necesiten diferentes versiones incompatibles (por ejemplo, dos versiones mayores diferentes). Si omitiéramos strictVersion o lo pusiéramos en false, webpack sólo emitiría una advertencia en tiempo de ejecución.

Hablaremos en otro articulos de como gestinoar las versiones en esta serie.

La propiedad requiredVersion: 'auto' es un pequeño extra proporcionado por el plugin @angular-architects/module-federation. Este busca la versión utilizada en tu package.json y  evita varios problemas.

Al asignar el valor auto en requiredVersion, este remplaza el valor  'auto' con la versión encontrada en tu package.json.

El Micro frontend (aka Remote)

El Micro frontend también llamado remote en Module Federation el cual es una aplicacion de Angular ordinaria. Tiene rutas definidas dentro del AppModule:

export const APP_ROUTES: Routes = [
    { path: '', component: HomeComponent, pathMatch: 'full'}
];

Además, el FlightsModule:

@NgModule({
  imports: [
    CommonModule,
    RouterModule.forChild(FLIGHTS_ROUTES)
  ],
  declarations: [
    FlightsSearchComponent
  ]
})
export class FlightsModule { }

Este módulo tiene algunas rutas propias:

export const FLIGHTS_ROUTES: Routes = [
    {
      path: 'flights-search',
      component: FlightsSearchComponent
    }
];

Para que sea posible cargar el FlightsModule en el shell, también necesitamos exponerlo a través de la configuración de webpack del remote:

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

module.exports = withModuleFederationPlugin({

  name: 'mfe1',

  exposes: {
    './Module': './projects/mfe1/src/app/flights/flights.module.ts',
  },

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

});

La configuración mostrada aquí expone el FlightsModule bajo el nombre público Module. La sección shared apunta a las librerias compartidas con el shell.

Iniciando el Micro frontend

Para probar todo, sólo tenemos que iniciar el shell y el micro frontend:

ng serve shell -o
ng serve mfe1 -o

Entonces, al hacer clic en Flights en el shell, se carga el micro frontend:

Sugerencia: También puedes usar el script npm run:all este se instala con schematics  ng-add y init:
npm run run:all

Para iniciar sólo algunas aplicaciones, añada sus nombres como argumentos:

npm run run:all shell mfe1

Mirando los detalles

Vale, eso ha funcionado bastante bien. ¿Pero has echado un vistazo a tu main.ts?

Se parece a esto:

import('./bootstrap')
    .catch(err => console.error(err));

El código que normalmente se encuentra en el archivo main.ts fue trasladado al archivo bootstrap.ts. Todo esto fue hecho por el plugin @angular-architects/module-federation.

Aunque esto no parece tener mucho sentido a primera vista, es un patrón típico que se encuentra en las aplicaciones basadas en Module Federation. La razón es que Module Federation necesita decidir qué versión de una biblioteca compartida cargar.

Si el shell, por ejemplo, está usando la versión 12.0 y uno de los micro frontends ya está construido con la versión 12.1, decidirá cargar esta última.

Para buscar los metadatos necesarios para tomar esta decision, Module Fedaration extra las importaciones dinámicas como esta de aquí. A diferencia de las importaciones estáticas más tradicionales, las importaciones dinámicas son asíncronas.

Por lo tanto, Module Federation puede decidir sobre las versiones a utilizar y cargarlas realmente.

Más detalles: Compartir dependencias

Como se ha mencionado anteriormente, el uso de shareAll permite una primera configuración rápida que "simplemente funciona". Sin embargo, puede conducir a demasiados bundles compartidos. Dado que las dependencias compartidas no pueden treeshaken y por defecto terminan en bundles separados que necesitan ser cargados, es posible que desee optimizar este comportamiento cambiando de shareAll a share:

// Import share instead of shareAll:
const { share, withModuleFederationPlugin } = require('@angular-architects/module-federation/webpack');

module.exports = withModuleFederationPlugin({

    // Explicitly share packages:
    shared: share({
        "@angular/core": { singleton: true, strictVersion: true, requiredVersion: 'auto' }, 
        "@angular/common": { singleton: true, strictVersion: true, requiredVersion: 'auto' }, 
        "@angular/common/http": { singleton: true, strictVersion: true, requiredVersion: 'auto' },                     
        "@angular/router": { singleton: true, strictVersion: true, requiredVersion: 'auto' },
    }),

});

Conclusión

La implementación de Micro frontend ha implicado hasta ahora numerosos trucos y soluciones. Module Federation de Webpack proporciona por fin una solución sencilla y sólida para ello. Para mejorar el rendimiento, las librerías que pueden ser compartidas y se pueden configurar estrategias para tratar con versiones incompatibles.

También es interesante que los micro frontend sean cargados por Webpack y sin referencias en el código fuente del host o del remoto. Esto simplifica el uso de la Module Federation y el código fuente resultante, que no requiera frameworks de micro frontend adicionales.

Sin embargo, este enfoque también pone más responsabilidad en los desarrolladores. Por ejemplo, hay que asegurarse de que los componentes que sólo se cargan en tiempo de ejecución y que aún no se conocían en el momento de la compilación también interactúen como se desea.

También hay que lidiar con posibles conflictos de versión. Por ejemplo, es probable que componentes que fueron compilados con versiones de Angular completamente diferentes no funcionen juntos en tiempo de ejecución. Estos casos deben ser evitados con convenciones o al menos reconocidos lo antes posible con pruebas de integración.

Traducción en español del artículo original de Manfred Steyer "The Microfrontend Revolution: Module Federation with Angular" actualizado el 06-06-2022
Plataforma de cursos gratis sobre programación