Module Federation Dinámico con Angular
Tags: microfrontends, angular, frontend, javascript
Published: 2022-07-27 05:10:00

> Traducción en español del artículo original de [Manfred Steyer](https://twitter.com/ManfredSteyer) "[Dynamic Module Federation with Angular](https://www.angulararchitects.io/en/aktuelles/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](https://www.angulararchitects.io/wp-content/webp-express/webp-images/doc-root/wp-content/uploads/2022/06/overview-cli14.png.webp)

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)](https://github.com/manfredsteyer/module-federation-with-angular-dynamic/tree/simple)

[📂 Código fuente (versión completa)](https://github.com/manfredsteyer/module-federation-with-angular-dynamic)

## 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:

```bash
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
```

## 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.
```bash
ng g @angular-architects/module-federation --project shell --port 4200 --type dynamic-host
```
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:

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

> 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:

```typescript
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));
```

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:

```typescript
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)
    },
];
```
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:

```javascript
// 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' }),
  },

});
```

```javascript
// 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' }),
  },

});
```
## Creando la navegacion 
Para cada ruta que carga un Micro Frontend, el AppComponent del shell contiene un routerLink:

```html
<!-- 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>
```

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:

```json
{
    "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"
    }
}
```

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:

```typescript
// 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>;
```
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í:

```typescript
// 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];
}
```

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

La shell  `AppComponent` se encarga de unir todo:

```typescript
@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);
  }
}
```
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ú:

```html
<!-- 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>
```

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:

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

`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:

```typescript
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));
```
`LoadRemoteModule(...)`: Si no quieres usar el manifiesto, puedes cargar directamente un Micro Frontend con loadRemoteModule:

```typescript
{
    path: 'flights',
    loadChildren: () =>
        loadRemoteModule({
            type: 'module',
            remoteEntry: 'http://localhost:4201/remoteEntry.js',
            exposedModule: './Module',
        }).then((m) => m.FlightsModule),
},
```
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:

```json
{
    "non-cli-13-stuff": {
        "type": "script",
        "remoteEntry": "http://localhost:4201/remoteEntry.js"
    }
}
```
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