Module Federation Dinámico con Angular

ngcontent

ng-content

Posted on July 27, 2022

Module Federation Dinámico con Angular

Traducción en español del artículo original de Manfred Steyer "Dynamic Module Federation with Angular" actualizado el 09-06-2022

En el artículo anterior de esta serie, he mostrado cómo utilizar Webpack Module Federation para cargar Micro frontends compilados por separado en un shell. Como la configuración de webpack del shell describe los Micro Frontendsya definidos.

En este artículo, estoy asumiendo una situación más dinámica donde el shell no conoce el Micro Frontend por adelantado. En su lugar, esta información se proporciona en tiempo de ejecución a través de un archivo de configuración. Mientras que este archivo es un archivo JSON estático en los ejemplos mostrados aquí, su contenido también podría venir de una API Web.

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

La siguiente imagen muestra la idea descrita en este artículo:

Configuración Microfrontend

Este es el ejemplo de configuracion de los Micro Frontends de los que el shell necesita encontrar en tiempo de ejecución, estos se estan mostrado en el menú y al hacer clic en él, este es cargado y mostrado por el router del la shell.

📂 Código fuente (versión simple, branch: simple)

📂 Código fuente (versión completa)

Dinamico y simple

Vamos a empezar con un enfoque simple. Para esto, asumimos que conocemos los Micro Frontends por adelantado y solamente queremos cambiar sus URLs en tiempo de ejecución, por ejemplo, con respecto al entorno actual. Un enfoque más avanzado, en el que ni siquiera necesitamos conocer el número de Micro Frontends por adelantado, se presenta a continuación.

Agregando Modulo Federation

El proyecto de demostración que utilizamos contiene un shell y dos Micro Frontends llamados mfe1 y mfe2. Como en el artículo anterior, añadimos e inicializamos el plugin Module Federation para los Micro Frontends:



npm i -g @angular-architects/module-federation -D

ng g @angular-architects/module-federation --project mfe1 --port 4201 --type remote

ng g @angular-architects/module-federation --project mfe2 --port 4202 --type remote


Enter fullscreen mode Exit fullscreen mode

Generación de un Manifiesto

A partir de la versión 14.3 del plugin, podemos generar un host dinámico que toma los datos escenciales sobre los Micro Frontend de un archivo json.



ng g @angular-architects/module-federation --project shell --port 4200 --type dynamic-host


Enter fullscreen mode Exit fullscreen mode

Esto genera una configuración de webpack, el manifiesto y agrega código en el main.ts para cargar el manifiesto que se encuentra projects/shell/src/assets/mf.manifest.json.

El manifiesto contiene la siguiente definicion:



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


Enter fullscreen mode Exit fullscreen mode

Es importante, después de generar el manifiesto, asegúrarnos que los puertos coinciden.

Cargando el manifiesto

El archivo main.ts generado carga el manifiesto:



import { loadManifest } from '@angular-architects/module-federation';

loadManifest("/assets/mf.manifest.json")
  .catch(err => console.error(err))
  .then(_ => import('./bootstrap'))
  .catch(err => console.error(err));


Enter fullscreen mode Exit fullscreen mode

Por defecto, loadManifest no sólo carga el manifiesto sino también las entradas remotas a las que apunta el manifiesto. Por lo tanto, Module Federation obtiene todos los metadatos necesarios para obtener los Micro Frontends bajo demanda.

Carga de los Micro Frontends

Para cargar los Micro Frontends descritos por el manifiesto, utilizamos las siguientes rutas:



export const APP_ROUTES: Routes = [
    {
      path: '',
      component: HomeComponent,
      pathMatch: 'full'
    },
    {
      path: 'flights',
      loadChildren: () => loadRemoteModule({
          type: 'manifest',
          remoteName: 'mfe1',
          exposedModule: './Module'
        })
        .then(m => m.FlightsModule)
    },
    {
      path: 'bookings',
      loadChildren: () => loadRemoteModule({
          type: 'manifest',
          remoteName: 'mfe2',
          exposedModule: './Module'
        })
        .then(m => m.BookingsModule)
    },
];


Enter fullscreen mode Exit fullscreen mode

La opción type: 'manifest' hace que loadRemoteModule busque los datos clave necesarios en el manifiesto cargado y la propiedad remoteName apunta a la clave que se utilizó en el manifiesto.

Configuración de los Micro Frontends

Esperamos que ambos Micro Frontends proporcionen un NgModule con sub-rutas a través de './Module'. Los NgModules se exponen a través del webpack.config.js en los Micro Frontends:



// projects/mfe1/webpack.config.js

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

module.exports = withModuleFederationPlugin({

  name: 'mfe1',

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

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

});


Enter fullscreen mode Exit fullscreen mode


// projects/mfe2/webpack.config.js

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

module.exports = withModuleFederationPlugin({

  name: 'mfe2',

  exposes: {
    // Adjusted line:
    './Module': './projects/mfe2/src/app/bookings/bookings.module.ts'
  },

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

});


Enter fullscreen mode Exit fullscreen mode

Creando la navegacion

Para cada ruta que carga un Micro Frontend, el AppComponent del shell contiene un routerLink:



<!-- projects/shell/src/app/app.component.html -->
<ul>
    <li><img src="../assets/angular.png" width="50"></li>
    <li><a routerLink="/">Home</a></li>
    <li><a routerLink="/flights">Flights</a></li>
    <li><a routerLink="/bookings">Bookings</a></li>
</ul>

<router-outlet></router-outlet>


Enter fullscreen mode Exit fullscreen mode

Eso es todo. Simplemente inicie los tres proyectos (por ejemplo, utilizando npm run run:all). La principal diferencia con el resultado del artículo anterior es que ahora el shell se informa a sí mismo sobre los Micro Frontends en tiempo de ejecución. Si quieres apuntar el shell a diferentes Micro Frontends, sólo tienes que ajustar el manifiesto.

Configurando las rutas Dinamicas

La solución que tenemos hasta ahora es adecuada en muchas situaciones: El uso del manifiesto permite ajustarlo a diferentes entornos sin reconstruir la aplicación. Además, si cambiamos el manifiesto por un servicio REST dinámico, podríamos implementar estrategias como el A/B testing.

Sin embargo, en algunas situaciones es posible que ni siquiera se conozca el número de Micro Frontends por adelantado. Esto es lo que discutimos aquí.

Añadiendo Metadatos Personalizados al Manifiesto

Para configurar dinámicamente las rutas, necesitamos algunos metadatos adicionales. Para ello, es posible que desee ampliar el manifiesto:



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

        "exposedModule": "./Module",
        "displayName": "Flights",
        "routePath": "flights",
        "ngModuleName": "FlightsModule"
    },
    "mfe2": {
        "remoteEntry": "http://localhost:4202/remoteEntry.js",

        "exposedModule": "./Module",
        "displayName": "Bookings",
        "routePath": "bookings",
        "ngModuleName": "BookingsModule"
    }
}


Enter fullscreen mode Exit fullscreen mode

Además de remoteEntry, todas las demás propiedades son personalizadas.

Tipos para la Configuración Extendida

Para representar nuestra configuración extendida, necesitamos algunos tipos que usaremos en la shell:



// projects/shell/src/app/utils/config.ts

import { Manifest, RemoteConfig } from "@angular-architects/module-federation";

export type CustomRemoteConfig = RemoteConfig & {
    exposedModule: string;
    displayName: string;
    routePath: string;
    ngModuleName: string;
};

export type CustomManifest = Manifest<CustomRemoteConfig>;


Enter fullscreen mode Exit fullscreen mode

El tipo CustomRemoteConfig representa las entradas del manifiesto y el tipo CustomManifest el manifiesto completo.

Creación dinámica de rutas

Ahora, necesitamos una función que itere a través de todo el manifiesto y cree una ruta para cada Micro Frontend descrito allí:



// projects/shell/src/app/utils/routes.ts

import { loadRemoteModule } from '@angular-architects/module-federation';
import { Routes } from '@angular/router';
import { APP_ROUTES } from '../app.routes';
import { CustomManifest } from './config';

export function buildRoutes(options: CustomManifest): Routes {

    const lazyRoutes: Routes = Object.keys(options).map(key => {
        const entry = options[key];
        return {
            path: entry.routePath,
            loadChildren: () => 
                loadRemoteModule({
                    type: 'manifest',
                    remoteName: key,
                    exposedModule: entry.exposedModule
                })
                .then(m => m[entry.ngModuleName])
        }
    });

    return [...APP_ROUTES, ...lazyRoutes];
}


Enter fullscreen mode Exit fullscreen mode

Esto nos da la misma estructura, que configuramos directamente arriba.

La shell AppComponent se encarga de unir todo:



@Component({
  selector: 'app-root',
  templateUrl: './app.component.html'
})
export class AppComponent implements OnInit  {

  remotes: CustomRemoteConfig[] = [];

  constructor(
    private router: Router) {
  }

  async ngOnInit(): Promise<void> {
    const manifest = getManifest<CustomManifest>();

    // Hint: Move this to an APP_INITIALIZER 
    //  to avoid issues with deep linking
    const routes = buildRoutes(manifest);
    this.router.resetConfig(routes);

    this.remotes = Object.values(manifest);
  }
}


Enter fullscreen mode Exit fullscreen mode

El método ngOnInit accede al manifiesto cargado (todavía está cargado en el main.ts como se muestra arriba) y lo pasa a la funcion buildRoutes. Las rutas dinámicas recuperadas se pasan al router y los valores de los pares clave/valor en el manifiesto, se ponen en el campo remotesm, estos se utilizan en el template para crear dinámicamente los elementos del menú:



<!-- projects/shell/src/app/app.component.html -->

<ul>
    <li><img src="../assets/angular.png" width="50"></li>
    <li><a routerLink="/">Home</a></li>

    <!-- Dynamically create menu items for all Micro Frontends -->
    <li *ngFor="let remote of remotes"><a [routerLink]="remote.routePath">{{remote.displayName}}</a></li>

    <li><a routerLink="/config">Config</a></li>
</ul>

<router-outlet></router-outlet>


Enter fullscreen mode Exit fullscreen mode

Ahora, probemos esta solución "dinámica" iniciando el shell y los Micro Frontends (por ejemplo, con npm run run:all).

Algunos detalles más

Hasta ahora, hemos utilizado las funciones de alto nivel proporcionadas por el plugin. Sin embargo, para los casos en los que necesites más control, también hay algunas alternativas de bajo nivel:

loadManifest(...): La función loadManifest utilizada anteriormente proporciona un segundo parámetro llamado skipRemoteEntries. Asignarlo a true evita la carga de los puntos de entrada. En este caso, sólo se carga el manifiesto:



loadManifest("/assets/mf.manifest.json", true)
    .catch(...)
    .then(...)
    .catch(...)


Enter fullscreen mode Exit fullscreen mode

setManifest(...): Esta función permite establecer directamente el manifiesto. Es muy útil si se cargan los datos desde otro lugar.

loadRemoteEntry(...): Esta función permite cargar directamente el punto de entrada remoto. Es útil si no se utiliza el manifiesto:



Promise.all([
    loadRemoteEntry({ type: 'module', remoteEntry: 'http://localhost:4201/remoteEntry.js' }),
    loadRemoteEntry({ type: 'module', remoteEntry: 'http://localhost:4202/remoteEntry.js' })
])
.catch(err => console.error(err))
.then(_ => import('./bootstrap'))
.catch(err => console.error(err));


Enter fullscreen mode Exit fullscreen mode

LoadRemoteModule(...): Si no quieres usar el manifiesto, puedes cargar directamente un Micro Frontend con loadRemoteModule:



{
    path: 'flights',
    loadChildren: () =>
        loadRemoteModule({
            type: 'module',
            remoteEntry: 'http://localhost:4201/remoteEntry.js',
            exposedModule: './Module',
        }).then((m) => m.FlightsModule),
},


Enter fullscreen mode Exit fullscreen mode

En general, creo que la mayoría de la gente utilizará el manifiesto en el futuro. Incluso si uno no quiere cargarlo desde un archivo JSON con loadManifest, puede definirlo mediante setManifest.

La propiedad type:'module' define que se quiere cargar un módulo EcmaScript "real" en lugar de "sólo" un archivo JavaScript. Esto es necesario desde Angular CLI 13. Si cargas cosas no construidas es muy probable que tengas que establecer esta propiedad como script. Esto también puede ocurrir a través del manifiesto:



{
    "non-cli-13-stuff": {
        "type": "script",
        "remoteEntry": "http://localhost:4201/remoteEntry.js"
    }
}


Enter fullscreen mode Exit fullscreen mode

Si una entrada del manifiesto no contiene una propiedad de type, el plugin asume el valor module.

Conclusión

Usar Module Federation dinamicos proporciona más flexibilidad ya que permite cargar Micro Frontends que no tenemos que conocer en tiempo de compilación. Ni siquiera tenemos que conocer su número por adelantado. Esto es posible gracias a la API en tiempo de ejecución proporcionada por webpack. Para hacer su uso un poco más fácil, el plugin @angular-architects/module-federation lo envuelve muy bien para simplificanos el trabajo.

Photo by Polina Sushko on Unsplash

💖 💪 🙅 🚩
ngcontent
ng-content

Posted on July 27, 2022

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related

Module Federation Dinámico con Angular
microfrontends Module Federation Dinámico con Angular

July 27, 2022