La Revolución de Micro Frontend: Module Federation con Angular
Tags: angular, frontend, microfrontend
Published: 2022-07-27 05:07:24

> Traducción en español del artículo original de Manfred Steyer "[The Microfrontend Revolution: Module Federation with Angular](https://www.angulararchitects.io/en/aktuelles/the-microfrontend-revolution-part-2-module-federation-with-angular/)" actualizado el 06-06-2022

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

En el [artículo anterior](https://ng-content.com/la-revolucion-del-microfrontend-module-federation-en-webpack-5), 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.

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

![Shell](https://www.angulararchitects.io/wp-content/uploads/2022/06/shell.png)

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

![Micro frontend without Shell](https://www.angulararchitects.io/wp-content/uploads/2022/06/mfe1.png)

 Puede descargar el codigo desde [Github](https://github.com/manfredsteyer/module-federation-plugin-example/tree/static) 

## 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](https://www.npmjs.com/package/@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` :

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

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

```typescript
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/Modul`e 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:

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

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

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

Además, el FlightsModule:
```typescript
@NgModule({
  imports: [
    CommonModule,
    RouterModule.forChild(FLIGHTS_ROUTES)
  ],
  declarations: [
    FlightsSearchComponent
  ]
})
export class FlightsModule { }
```

Este módulo tiene algunas rutas propias:

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

```json
ng serve shell -o
ng serve mfe1 -o
```

Entonces, al hacer clic en Flights en el shell, se carga el micro frontend:
![Shell](https://www.angulararchitects.io/wp-content/uploads/2022/06/result.png)


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

```bash
npm run run:all
```
![run:all script](https://www.angulararchitects.io/wp-content/uploads/2022/06/run-all.png)

Para iniciar sólo algunas aplicaciones, añada sus nombres como argumentos:
```bash
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:

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

```javascript
// 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 librerias 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.