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?
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
:
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
.
Tratamiento de errores en mfe1
Sin embargo, en mfe1
, nos encontramos con un error:
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
.
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.
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.