Angular con Native Federation

Importante: Para que todo funcionara, utilicé la versión 18.0.0 de @angular-architects/native-federation. Actualmente, la versión 18.0.2 tiene un bug que impide la importación dinámica.

Importando componentes dinámicos

Para empezar, he preparado un componente dentro de mfe2 que vamos a exportar: animated-box.component. Este componente contiene una caja animada y ya se está utilizando dentro de mfe2. Sin embargo, ahora tenemos que importarlo para utilizarlo en mfe1. ¿Cómo lo hacemos?

CPU
1 vCPU
MEMORIA
1 GB
ALMACENAMIENTO
10 GB
TRANSFERENCIA
1 TB
PRECIO
$ 4 mes
Para obtener el servidor GRATIS debes de escribir el cupon "LEIFER"
Angular con Native Federation

Paso 1: Exportar el componente en federation.config

En primer lugar, exportaremos nuestro componente de cuadro animado en el archivo federation .config:

 // federation.config.ts
exposes: {
  './Component': './src/app/app.component.ts',
  './AnimatedBox': './src/app/components/animated-box/animated-box.component.ts',
}

Paso 2: Crear un marcador de posición para el componente en mfe1

A continuación, en nuestro mfe1, crearemos un div que actuará como marcador de posición donde se insertará el componente. En crud.component.html:

<div #placeAnimatedBox></div>

Paso 3: Cargar el componente remoto

Ahora, procedamos a nuestro crud.component.ts y realicemos la carga remota del componente:

import { loadRemoteModule } from '@angular-architects/native-federation';
import { Component, OnInit, ViewChild, ViewContainerRef } from '@angular/core';
import { RouterModule } from '@angular/router';
import { CustomRouterLinkDirective } from '../directives/custom-router-link.directive';

@Component({
  selector: 'app-crud',
  standalone: true,
  imports: [RouterModule, CustomRouterLinkDirective],
  templateUrl: './crud.component.html',
  styleUrls: ['./crud.component.scss']
})
export class CrudComponent implements OnInit {

  @ViewChild('placeAnimatedBox', { read: ViewContainerRef })
  viewContainer!: ViewContainerRef;

  constructor() { }

  ngOnInit() {
    setTimeout(() => {
      this.loadAnimatedBox();
    }, 2000);
  }

  async loadAnimatedBox(): Promise<void> {
    const m = await loadRemoteModule({
      remoteEntry: 'http://localhost:4202/remoteEntry.json',
      exposedModule: './AnimatedBox'
    });

    const ref = this.viewContainer.createComponent(m.AnimatedBoxComponent);
    // const compInstance = ref.instance;
  }
}

Y como por arte de magia, podemos importar a mfe1 un componente creado y mantenido en mfe2:

Componentes exportados de MFE

Resolución de problemas de servicio en micro frontends Angular

Nuestro componente funciona porque es extremadamente simple. Sin embargo, si este componente empieza a depender de un servicio, dejará de funcionar. Para resolver esto, cambiemos ligeramente nuestro enfoque creando un servicio dentro de mfe2.

Creación del servicio en mfe2

En primer lugar, definimos un servicio dentro de mfe2:

import { HttpClient } from '@angular/common/http';
import { Injectable } from '@angular/core';

@Injectable()
export class DataService {

  constructor(private httpClient: HttpClient) { }

  fetchData() {
    return this.httpClient.get('https://jsonplaceholder.typicode.com/posts/1');
  }
}

Proporcionar el servicio en el componente de la aplicación

A continuación, tenemos que proporcionar este servicio en el app.component.ts:

import { Component } from '@angular/core';
import { RouterOutlet } from '@angular/router';
import { ExposeAnimatedBoxComponent } from './exposes/expose-animated-box/expose-animated-box.component';
import { FooComponent } from './pages/foo/foo.component';
import { DataService } from './services/data.service';

@Component({
  selector: 'app-root',
  standalone: true,
  imports: [RouterOutlet, FooComponent, ExposeAnimatedBoxComponent],
  providers: [DataService],
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.scss']
})
export class AppComponent {
  title = 'mfe2';
}

Como estamos usando HttpClient, debemos definir el proveedor en app.config.ts. También es recomendable hacer lo mismo en nuestro mfe1 y shell para asegurar que una instancia de HttpClient está disponible al importar el componente.

import { ApplicationConfig, provideZoneChangeDetection } from '@angular/core';
import { provideRouter } from '@angular/router';
import { provideHttpClient, withFetch } from '@angular/common/http';
import { routes } from './app.routes';

export const appConfig: ApplicationConfig = {
  providers: [
    provideZoneChangeDetection({ eventCoalescing: true }),
    provideRouter(routes),
    provideHttpClient(withFetch())
  ]
};

Uso del servicio en un componente

Ahora, vamos a llamar a fetchData dentro de nuestro animated-box.component.ts:

import { CommonModule } from '@angular/common';
import { Component, OnInit } from '@angular/core';
import { DataService } from '../../services/data.service';

@Component({
  selector: 'app-animated-box',
  standalone: true,
  imports: [CommonModule],
  templateUrl: './animated-box.component.html',
  styleUrls: ['./animated-box.component.scss']
})
export class AnimatedBoxComponent implements OnInit {

  constructor(private dataService: DataService) { }

  ngOnInit() {
    this.dataService.fetchData().subscribe((data) => {
      console.log("AnimatedBoxComponent: " + JSON.stringify(data));
    });
  }
}

Con esta configuración, la integración con el endpoint funciona dentro de mfe2.

Fetch data funciona dentro de mfe2

Tratamiento de errores en mfe1

Sin embargo, en mfe1, nos encontramos con un error:

MFE1 con errores 

Para resolver esto, en lugar de exportar nuestro AnimatedBoxComponent directamente, creamos un componente separado que proporcionará todos los módulos, servicios y proveedores necesarios que nuestro AnimatedBoxComponent necesita. Sugiero crear una carpeta separada para estos componentes intermedios, como /exposes.

Creando ExposeAnimatedBoxComponent

Dentro de la carpeta /exposes, vamos a crear un componente ExposeAnimatedBoxComponent:

<!-- expose-animated-box.component.html -->
<app-animated-box></app-animated-box>
// expose-animated-box.component.html
import { Component } from '@angular/core';
import { AnimatedBoxComponent } from '../../components/animated-box/animated-box.component';
import { DataService } from '../../services/data.service';

@Component({
  selector: 'app-expose-animated-box',
  standalone: true,
  imports: [AnimatedBoxComponent],
  providers: [DataService],
  templateUrl: './expose-animated-box.component.html',
  styleUrl: './expose-animated-box.component.scss'
})
export class ExposeAnimatedBoxComponent {

}

A continuación, exportaremos nuestro componente que encapsula las dependencias necesarias para AnimatedBoxComponent:

exposes: {
  './Component': './src/app/app.component.ts',
  './ExposeAnimatedBox': './src/app/exposes/expose-animated-box/expose-animated-box.component.ts',
},

Actualizando crud.component.ts

Por último, en nuestro crud.component.ts, vamos a actualizar la importación para utilizar el nuevo componente:

import { loadRemoteModule } from '@angular-architects/native-federation';
import { Component, OnInit, ViewChild, ViewContainerRef } from '@angular/core';
import { RouterModule } from '@angular/router';
import { CustomRouterLinkDirective } from '../directives/custom-router-link.directive';

@Component({
  selector: 'app-crud',
  standalone: true,
  imports: [RouterModule, CustomRouterLinkDirective],
  templateUrl: './crud.component.html',
  styleUrls: ['./crud.component.scss']
})
export class CrudComponent implements OnInit {

  @ViewChild('placeAnimatedBox', { read: ViewContainerRef })
  viewContainer!: ViewContainerRef;

  constructor() { }

  ngOnInit() {
    setTimeout(() => {
      this.loadAnimatedBox();
    }, 2000);
  }

  async loadAnimatedBox(): Promise<void> {
    const m = await loadRemoteModule({
      remoteEntry: 'http://localhost:4202/remoteEntry.json',
      exposedModule: './ExposeAnimatedBox'
    });

    const ref = this.viewContainer.createComponent(m.ExposeAnimatedBoxComponent);
  }
}

Con este enfoque, encapsulamos con éxito el AnimatedBoxComponent dentro de otro (ExposeAnimatedBoxComponent) que sirve para proporcionar los servicios y módulos necesarios, asegurando que se proporcionan correctamente en la raíz o el nivel superior de nuestro árbol de aplicación en mfe2.

Todos los trabajos 


Un gran poder conlleva una gran responsabilidad

La mayor ventaja de los micro frontales es la capacidad de descomponer una aplicación monolítica en partes más pequeñas y manejables, lo que permite a equipos independientes desarrollar, desplegar y mantener estas partes de forma aislada.

Esto aumenta la escalabilidad, facilita el mantenimiento y promueve la reutilización de componentes. Además, permite adoptar diferentes tecnologías según las necesidades, mejorando la flexibilidad del desarrollo.

Utilizando el enfoque de carga remota de componentes en Angular y micro front ends, es posible implementar funcionalidades y componentes mantenidos por otros equipos (squads).

Este enfoque tiene la ventaja de reflejar automáticamente las actualizaciones realizadas por el equipo responsable del componente. Sin embargo, esta ventaja también puede acarrear problemas si no se presta la debida atención al mantenimiento de los componentes exportados.

Es crucial asegurarse de que los componentes exportados son estables y compatibles para evitar fallos o incoherencias en la aplicación.

Bono

Al analizar el tamaño de las aplicaciones individualmente y luego cargadas dentro del shell, observamos que no se recargan completamente. Esto se debe a que Native Federation hace un excelente trabajo reutilizando el código de dependencias. Así, la carga remota se restringe sólo al código que realmente se escribió para su aplicación, optimizando el rendimiento y reduciendo el tiempo de carga.

Esta eficiencia en la gestión de dependencias permite que las aplicaciones sean más ligeras y rápidas, mejorando la experiencia del usuario.

Ejemplo 

Para una demostración práctica y para seguir los detalles de la implementación, puedes consultar el código fuente completo en GitHub.

El repositorio incluye todas las configuraciones necesarias y ejemplos de código para ayudarle a configurar y ejecutar el proyecto sin problemas. Visita el repositorio aquí.

Conclusión

Descomponer una aplicación monolítica en micro front-ends aporta varias ventajas, como escalabilidad, mantenimiento más sencillo y flexibilidad tecnológica.

Utilizando la carga remota de componentes en Angular, podemos reflejar automáticamente las actualizaciones realizadas por otros equipos, siempre que se cuide la estabilidad y compatibilidad de los componentes exportados. La federación nativa optimiza el rendimiento al reutilizar el código de dependencias, lo que se traduce en aplicaciones más ligeras y rápidas.

Fuente