Desarrollando Micro Frontends con Angular: Guía Completa de Módulos Federados y Monorepos
Este tutorial profundiza en la creación de arquitecturas de Micro Frontends en Angular, utilizando Módulos Federados de Webpack 5. Aprenderás a configurar un monorepo, compartir código entre aplicaciones y desplegar MFE de forma independiente, mejorando la escalabilidad y mantenibilidad de tus proyectos.
🚀 Introducción a los Micro Frontends en Angular
El desarrollo de aplicaciones web complejas ha llevado a la búsqueda de arquitecturas que permitan una mayor escalabilidad, independencia en el desarrollo y despliegue, y una mejor organización de equipos. Los Micro Frontends (MFE) emergen como una solución poderosa, aplicando los principios de los microservicios al lado del cliente.
En esencia, un Micro Frontend es una parte independiente y autónoma de una aplicación web más grande. Imagina una aplicación monolítica dividida en varias aplicaciones más pequeñas, cada una con su propio stack tecnológico (aunque en Angular, a menudo se comparte), su propio equipo de desarrollo y su propio ciclo de despliegue.
¿Por qué Micro Frontends? 🤔
Adoptar una arquitectura de Micro Frontends puede traer consigo una serie de beneficios significativos:
- Independencia en el Desarrollo: Diferentes equipos pueden trabajar en diferentes Micro Frontends de manera simultánea sin pisarse. Cada equipo es dueño de su parte de la aplicación, desde el desarrollo hasta el despliegue.
- Tecnologías Heterogéneas: Aunque en este tutorial nos centraremos en Angular, los MFE permiten combinar diferentes frameworks o versiones de los mismos dentro de una misma aplicación. Por ejemplo, una parte puede estar en Angular 15 y otra en Angular 16.
- Despliegue Independiente: Cada MFE puede ser desplegado de forma autónoma. Esto reduce el riesgo, ya que un fallo en un MFE no necesariamente afecta a toda la aplicación, y permite una mayor agilidad en la entrega de nuevas funcionalidades.
- Escalabilidad del Equipo: Facilita la incorporación de nuevos equipos o la reestructuración de los existentes, asignando responsabilidades claras a cada Micro Frontend.
- Mantenimiento Reducido: Las bases de código son más pequeñas y manejables, lo que simplifica el mantenimiento y la depuración.
🛠️ Herramientas Clave para Micro Frontends en Angular
Para construir Micro Frontends robustos y eficientes en Angular, nos apoyaremos en dos pilares fundamentales:
- Módulos Federados (Webpack 5): Esta es la característica estrella que nos permite compartir código y exponer componentes o módulos entre diferentes aplicaciones. Webpack 5, el bundler subyacente de Angular CLI, trae esta potente capacidad de serie.
- Monorepo (Nx): Una estrategia de monorepo nos permite gestionar múltiples proyectos (aplicaciones y librerías) dentro de un único repositorio de Git. Herramientas como Nx (Next Generation Build System) optimizan esta gestión, ofreciendo builds incrementales, análisis de dependencias y una experiencia de desarrollo unificada.
Módulos Federados: El Corazón del Compartir Código ✨
Los Módulos Federados (Module Federation) de Webpack 5 permiten a una aplicación JavaScript cargar código de otra aplicación JavaScript de forma dinámica, en tiempo de ejecución. Esto significa que puedes:
- Exponer (
expose) componentes, servicios o módulos de una aplicación para que otras los consuman. - Consumir (
remote) los elementos expuestos por otras aplicaciones. - Compartir (
shared) librerías comunes (como Angular, RxJS) para evitar que se carguen múltiples veces, optimizando el tamaño del bundle.
Monorepos con Nx: La Orquestación Perfecta 🎼
Nx es una extensión de Angular CLI (y otras herramientas como React, Node) que mejora significativamente la experiencia de desarrollo en monorepos. Ofrece:
- Generadores de Código: Crea nuevas aplicaciones, librerías y componentes con facilidad.
- Análisis de Impacto: Determina qué proyectos se ven afectados por un cambio, permitiendo builds y tests incrementales.
- Caché Distribuida: Reutiliza resultados de builds y tests anteriores, acelerando el desarrollo.
- Detección de Dependencias: Mapea las dependencias entre proyectos para una mejor comprensión de la arquitectura.
🧑💻 Configurando Nuestro Primer Monorepo con Nx
Vamos a empezar creando un nuevo monorepo de Nx. Este será el contenedor de nuestra aplicación shell (anfitrión) y nuestros Micro Frontends.
1. Instalación de Nx
Primero, si no lo tienes, instala el CLI de Nx globalmente:
npm install -g nx
2. Creación del Monorepo
Ahora, crea un nuevo workspace de Nx. Usaremos el preset angular.
npx create-nx-workspace my-mfe-workspace --preset=angular
Te hará algunas preguntas:
What to create in the new workspace: SeleccionaIntegrated monorepo.Application name:shell(será nuestra aplicación principal o host).Would you like to add routing to this application?:YesWhich stylesheet format would you like to use?:CSS(o tu preferencia)
Esto creará un directorio my-mfe-workspace con una estructura similar a esta:
my-mfe-workspace/
├── apps/
│ └── shell/ # Nuestra aplicación host
│ ├── src/
│ └── ...
├── libs/ # Para librerías compartidas
├── nx.json # Configuración de Nx
├── package.json # Dependencias del monorepo
└── tsconfig.base.json # Configuración de TypeScript
3. Ejecutando la Aplicación Host
Navega al directorio del workspace y ejecuta la aplicación shell:
cd my-mfe-workspace
npm start shell
Deberías ver la aplicación Angular corriendo en http://localhost:4200/.
📦 Creando y Federando un Micro Frontend
Ahora que tenemos nuestro host, vamos a crear un Micro Frontend independiente y configurarlo para que pueda ser cargado por nuestra aplicación shell.
1. Generando un Nuevo Micro Frontend
Nx facilita la creación de aplicaciones MFE con la configuración de Módulos Federados ya integrada. Usaremos el generador mfe:
nx g @nx/angular:mfe feature-products --host=shell --port=4201
Aquí:
feature-products: Es el nombre de nuestro nuevo Micro Frontend (ej. para funcionalidades de productos).--host=shell: Indica que esta aplicación será un remote (MFE) y queshellserá su host (quien la consumirá). Esto configura automáticamente las dependencias.--port=4201: El puerto donde se ejecutará este MFE de forma independiente.
Después de ejecutar este comando, verás una nueva aplicación feature-products en el directorio apps/. Nx habrá modificado automáticamente los archivos webpack.config.js y module-federation.config.ts tanto para shell como para feature-products.
2. Explorando la Configuración de Módulos Federados
Echemos un vistazo a los archivos clave generados:
apps/shell/module-federation.config.ts (Host)
import { ModuleFederationConfig } from '@nx/webpack/module-federation';
const config: ModuleFederationConfig = {
name: 'shell',
remotes: ['feature-products'], // Aquí se declara que 'shell' cargará 'feature-products'
shared: {
// ... dependencias compartidas ...
},
};
export default config;
El array remotes es crucial; aquí es donde declaramos qué Micro Frontends vamos a consumir. Nx ya ha añadido feature-products.
apps/feature-products/module-federation.config.ts (Remote)
import { ModuleFederationConfig } from '@nx/webpack/module-federation';
const config: ModuleFederationConfig = {
name: 'feature-products',
exposes: {
'./Module': 'apps/feature-products/src/app/remote-entry/entry.module.ts', // Lo que este MFE expone
},
shared: {
// ... dependencias compartidas ...
},
};
export default config;
El objeto exposes es donde definimos qué partes de nuestro MFE estarán disponibles para el host o para otros MFE. Por defecto, Nx expone un EntryModule que contiene el routing específico de este MFE.
3. Integrando el MFE en el Host
Ahora debemos configurar nuestra aplicación shell para que cargue el feature-products MFE. Esto se hace típicamente en el archivo de routing principal del host.
Edita apps/shell/src/app/app.routes.ts (o app-routing.module.ts si no usas standalone components):
import { loadRemoteModule } from '@nx/angular/mf';
import { Route } from '@angular/router';
export const appRoutes: Route[] = [
{
path: 'products',
loadChildren: () =>
loadRemoteModule('feature-products', './Module').then((m) => m.RemoteEntryModule),
},
// Otras rutas de la aplicación host
{
path: '',
redirectTo: 'products',
pathMatch: 'full',
}
];
Aquí estamos usando loadRemoteModule de Nx para cargar el módulo expuesto por feature-products. Cuando el usuario navegue a /products, Angular cargará dinámicamente el RemoteEntryModule de nuestro Micro Frontend feature-products.
4. Ejecutando la Arquitectura Completa
Para ver todo en acción, necesitamos ejecutar tanto el host como el remote simultáneamente. Nx facilita esto:
nx run-many --target=serve --projects=shell,feature-products --parallel
Esto iniciará:
- La aplicación
shellenhttp://localhost:4200/. - La aplicación
feature-productsenhttp://localhost:4201/.
Ahora, al navegar a http://localhost:4200/products en tu navegador, deberías ver el contenido del Micro Frontend feature-products cargado dentro de la aplicación shell. ¡Has creado tu primer Micro Frontend federado!
🤝 Compartiendo Código y Dependencias
Uno de los mayores beneficios de los Micro Frontends es la capacidad de compartir código común (componentes UI, servicios, modelos de datos, etc.) entre diferentes MFE y el host. Nx y Module Federation nos permiten hacerlo de manera eficiente.
1. Creando una Librería Compartida con Nx
En un monorepo, las librerías son el lugar ideal para el código reutilizable. Creamos una nueva librería para nuestros componentes UI compartidos:
nx g @nx/angular:library ui-components --directory=shared
Esto creará la librería ui-components dentro de libs/shared/ui-components.
Ejemplo: Un Componente de Botón Compartido
Vamos a crear un componente de botón simple dentro de libs/shared/ui-components/src/lib/button/button.component.ts:
import { Component, Input } from '@angular/core';
import { CommonModule } from '@angular/common';
@Component({
selector: 'app-button',
standalone: true,
imports: [CommonModule],
template: `
<button [ngClass]="type" (click)="onClick.emit()">
<ng-content></ng-content>
</button>
`,
styles: [`
button {
padding: 10px 20px;
border-radius: 5px;
border: none;
cursor: pointer;
font-size: 16px;
}
.primary { background-color: #007bff; color: white; }
.secondary { background-color: #6c757d; color: white; }
`]
})
export class ButtonComponent {
@Input() type: 'primary' | 'secondary' = 'primary';
onClick = new (require('events').EventEmitter)(); // Simplified for example
}
Exporta este componente desde libs/shared/ui-components/src/index.ts:
export * from './lib/button/button.component';
2. Consumiendo la Librería Compartida
Ahora, tanto el shell como feature-products pueden usar este componente de botón. Solo necesitas importarlo en sus módulos o componentes standalone.
En feature-products (ej. apps/feature-products/src/app/remote-entry/entry.component.ts):
import { Component } from '@angular/core';
import { CommonModule } from '@angular/common';
import { ButtonComponent } from '@my-mfe-workspace/shared/ui-components'; // Importa desde la librería
@Component({
selector: 'app-feature-products-entry',
standalone: true,
imports: [CommonModule, ButtonComponent],
template: `
<div class="remote-entry">
<h2>¡Bienvenido a Productos!</h2>
<p>Este es el Micro Frontend de productos.</p>
<app-button type="primary" (click)="handleBuy()">Comprar Ahora</app-button>
</div>
`,
styles: [
`
.remote-entry {
background-color: #e0f7fa;
padding: 20px;
border-radius: 8px;
margin: 20px;
}
`,
],
})
export class RemoteEntryComponent {
handleBuy() {
alert('¡Producto comprado desde el MFE de Productos!');
}
}
En el shell (ej. apps/shell/src/app/app.component.ts):
import { Component } from '@angular/core';
import { RouterModule } from '@angular/router';
import { NxWelcomeComponent } from './nx-welcome.component';
import { ButtonComponent } from '@my-mfe-workspace/shared/ui-components'; // Importa desde la librería
@Component({
standalone: true,
imports: [NxWelcomeComponent, RouterModule, ButtonComponent],
selector: 'app-root',
template: `
<h1>Aplicación Principal</h1>
<nav>
<a routerLink="/products">Ir a Productos</a>
</nav>
<router-outlet></router-outlet>
<hr>
<app-button type="secondary">Botón del Host</app-button>
`,
styles: [],
})
export class AppComponent {
title = 'shell';
}
Cuando ejecutes nx run-many --target=serve --projects=shell,feature-products --parallel, verás que ambos, el host y el MFE, utilizan el mismo ButtonComponent de la librería compartida.
3. Compartiendo Dependencias de Webpack (Automaticamente por Nx)
Module Federation es inteligente con las dependencias. Cuando Nx crea un MFE, configura automáticamente las dependencias compartidas en los archivos module-federation.config.ts. Esto asegura que librerías grandes como Angular, RxJS, y Zone.js se carguen una única vez, incluso si múltiples MFE las utilizan.
Por ejemplo, si revisas los archivos de configuración de Módulos Federados, verás una sección shared que gestiona esto:
// Ejemplo de shared en module-federation.config.ts
shared: {
'@angular/core': { requiredVersion: '^16.2.0', singleton: true, strictVersion: true, eager: true },
'@angular/common': { requiredVersion: '^16.2.0', singleton: true, strictVersion: true, eager: true },
// ... otras dependencias de Angular y Nx ...
}
singleton: true: Asegura que solo se cargue una instancia de la dependencia en tiempo de ejecución, incluso si múltiples remotes la requieren.strictVersion: true: Si estrue, Webpack verificará que las versiones de las dependencias sean compatibles. Si hay una incompatibilidad, emitirá una advertencia o un error.requiredVersion: La versión mínima requerida de la dependencia.
🌐 Estrategias de Comunicación entre Micro Frontends
La comunicación es un aspecto crucial en una arquitectura de Micro Frontends. Aunque buscamos independencia, a menudo los MFE necesitan intercambiar información. Hay varias estrategias, cada una con sus pros y contras.
1. Comunicación a Través del DOM (Eventos Personalizados)
Una de las formas más desacopladas es usar eventos del DOM. Los MFE pueden disparar eventos personalizados, y otros MFE o el host pueden escucharlos.
MFE disparando un evento:
// En un componente del MFE 'feature-products'
import { Component } from '@angular/core';
@Component({
// ...
template: `
<button (click)="notifyHost()">Notificar Host</button>
`,
})
export class ProductComponent {
notifyHost() {
const event = new CustomEvent('product-selected', {
detail: { productId: 'P123', productName: 'Producto X' },
bubbles: true, // El evento puede burbujear a elementos padre en el DOM
composed: true, // El evento puede cruzar límites de shadow DOM
});
window.dispatchEvent(event); // O this.el.nativeElement.dispatchEvent(event);
}
}
Host escuchando el evento:
// En el app.component.ts del 'shell'
import { Component, OnInit, OnDestroy } from '@angular/core';
import { fromEvent, Subject } from 'rxjs';
import { takeUntil } from 'rxjs/operators';
@Component({
// ...
})
export class AppComponent implements OnInit, OnDestroy {
private destroy$ = new Subject<void>();
ngOnInit() {
fromEvent(window, 'product-selected')
.pipe(takeUntil(this.destroy$))
.subscribe((event: Event) => {
const customEvent = event as CustomEvent;
console.log('Evento de producto recibido en el host:', customEvent.detail);
// Realizar alguna acción con los datos
});
}
ngOnDestroy() {
this.destroy$.next();
this.destroy$.complete();
}
}
2. Comunicación a Través de Servicios Compartidos (Librerías)
Para una comunicación más estructurada o para compartir estados, puedes definir servicios en una librería compartida. Sin embargo, esto requiere que todos los MFE que utilicen este servicio sean conscientes de él y lo importen.
En libs/shared/data-access/src/lib/product.service.ts:
import { Injectable } from '@angular/core';
import { BehaviorSubject, Observable } from 'rxjs';
interface Product {
id: string;
name: string;
}
@Injectable({ providedIn: 'root' })
export class ProductService {
private selectedProductSubject = new BehaviorSubject<Product | null>(null);
selectedProduct$: Observable<Product | null> = this.selectedProductSubject.asObservable();
selectProduct(product: Product) {
this.selectedProductSubject.next(product);
}
clearSelection() {
this.selectedProductSubject.next(null);
}
}
En un MFE (feature-products):
// app.component.ts en feature-products
import { Component } from '@angular/core';
import { ProductService } from '@my-mfe-workspace/shared/data-access';
@Component({
// ...
template: `
<button (click)="selectProduct()">Seleccionar Producto para Host</button>
`,
})
export class RemoteEntryComponent {
constructor(private productService: ProductService) {}
selectProduct() {
this.productService.selectProduct({ id: 'PROD_001', name: 'Super Widget MFE' });
}
}
En el Host (shell):
// app.component.ts en shell
import { Component, OnInit } from '@angular/core';
import { ProductService } from '@my-mfe-workspace/shared/data-access';
@Component({
// ...
})
export class AppComponent implements OnInit {
currentProduct: string | null = null;
constructor(private productService: ProductService) {}
ngOnInit() {
this.productService.selectedProduct$.subscribe(product => {
this.currentProduct = product ? product.name : 'Ninguno';
console.log('Producto seleccionado recibido en el host:', product);
});
}
}
3. Comunicación a Través de URL (Parámetros de Ruta)
Para pasar datos simples o IDs, los parámetros de ruta son una opción sencilla y efectiva, ideal para la navegación entre MFE.
// Desde el host navegando a un MFE
this.router.navigate(['/products', productId]);
// En el MFE, obtener el parámetro
import { ActivatedRoute } from '@angular/router';
// ...
constructor(private route: ActivatedRoute) {
this.route.params.subscribe(params => {
console.log('ID de producto recibido:', params['id']);
});
}
Intermedio
🚀 Despliegue de Micro Frontends
El despliegue independiente es una de las grandes promesas de los Micro Frontends. Cada MFE se construye y se despliega como una aplicación estática separada. El host es el encargado de saber dónde encontrar estos remotes.
1. Construyendo los Micro Frontends para Producción
Cada aplicación (host y remotes) se construye de forma independiente. Nx facilita esto:
nx build shell --prod
nx build feature-products --prod
Esto generará los bundles optimizados en dist/apps/shell y dist/apps/feature-products.
2. Configuración de Servidor Web (Nginx/Apache)
Para que los Módulos Federados funcionen, tu servidor web debe ser capaz de servir los archivos estáticos de cada MFE y del host. Además, el host necesita saber la URL base de cada remote.
Cuando los MFE se construyen, el archivo remoteEntry.js (o similar) se genera con la ruta absoluta donde se espera que se encuentre el MFE. Por defecto, puede apuntar a http://localhost:4201/remoteEntry.js.
Para producción, necesitas actualizar dinámicamente esta URL. Nx proporciona un plugin para Webpack que permite configurar esto a través del module-federation.config.ts:
// apps/shell/module-federation.config.ts
import { ModuleFederationConfig } from '@nx/webpack/module-federation';
const config: ModuleFederationConfig = {
name: 'shell',
remotes: [
// Aquí puedes usar una variable de entorno para la URL de producción
['feature-products', 'http://localhost:4201/remoteEntry.js'], // Desarrollo
// 'feature-products@https://cdn.tuservidor.com/feature-products/remoteEntry.js' // Producción
],
// ...
};
export default config;
Una forma común es tener un archivo de configuración (environment.prod.ts) donde se definan las URLs de los remotes para producción, y que sea cargado por el host en tiempo de ejecución.
Ejemplo de Configuración de Nginx
Si tu host se sirve desde /, y tus MFE desde /feature-products/:
server {
listen 80;
server_name yourdomain.com;
# Configuración para la aplicación Host
location / {
root /usr/share/nginx/html/shell;
try_files $uri $uri/ /index.html;
}
# Configuración para el MFE de Productos
location /feature-products/ {
root /usr/share/nginx/html/feature-products;
try_files $uri $uri/ /index.html;
}
}
Consideraciones Adicionales de Despliegue
- Versiones de MFE: Cómo manejar el despliegue de nuevas versiones de MFE sin afectar a la aplicación principal. Estrategias como versioning en la URL (
/v2/feature-products/) o el uso de hashes en el nombre del bundle son comunes. - CDN: Servir los MFE desde una CDN puede mejorar el rendimiento global.
- CI/CD: Implementar pipelines de integración continua y despliegue continuo para cada MFE y para el host es fundamental para la agilidad.
- Manejo de Errores: Qué sucede si un MFE falla al cargar. El host debe tener mecanismos de fallback para mostrar un mensaje de error o una versión alternativa.
3. Orquestación del Despliegue
La orquestación se refiere a cómo se gestionan los despliegues de múltiples Micro Frontends. En un monorepo, puedes tener un único pipeline de CI/CD que, al detectar cambios en un MFE específico, solo reconstruya y despliegue ese MFE, o también puedes tener pipelines independientes para cada MFE.
Un buen enfoque es:
- Cada MFE se construye y despliega de forma independiente a su propia ubicación (ej. un bucket de S3, un servidor específico) o ruta en un CDN.
- El host se despliega por separado, y su configuración apunta a las ubicaciones de los MFE remotos. Idealmente, el host lee estas ubicaciones desde variables de entorno o un servicio de configuración.
✅ Buenas Prácticas y Consideraciones Finales
Adoptar Micro Frontends es un cambio arquitectónico significativo. Aquí hay algunas buenas prácticas y consideraciones finales para asegurar el éxito de tu implementación:
- Definir Límites Claros: Cada Micro Frontend debe tener una responsabilidad clara y bien definida. Evita la tentación de que un MFE haga demasiado.
- Independencia Tecnológica (con Cautela): Aunque Module Federation lo permite, mezclar demasiados frameworks o versiones de Angular puede aumentar la complejidad. Busca un equilibrio.
- Contratos de Comunicación: Si los MFE se comunican, define contratos claros (APIs de eventos, interfaces de servicios) para evitar dependencias ocultas y facilitar la evolución.
- UI/UX Consistente: Asegura una experiencia de usuario unificada. Esto a menudo se logra a través de una librería de componentes de UI compartida o un design system.
- Observabilidad: Implementa una monitorización y logging unificados para poder rastrear problemas a través de los diferentes MFE.
- Pruebas de Integración: Además de las pruebas unitarias y de E2E de cada MFE, son cruciales las pruebas de integración que involucren al host y a varios MFE.
- Gestión de Dependencias Compartidas: Presta atención a las versiones de librerías compartidas. Un conflicto de versiones puede causar problemas sutiles y difíciles de depurar.
- Tamaño del Bundle: Aunque Module Federation optimiza, sigue monitorizando el tamaño de los bundles cargados. Cargar demasiados MFE o dependencias innecesarias puede afectar el rendimiento.
- Manejo de Errores y Fallbacks: Planifica qué sucede si un MFE falla al cargar o al ejecutar. ¿El host mostrará un mensaje de error? ¿Intentará cargar una versión anterior?
Los Micro Frontends, especialmente con la potencia de Module Federation en Angular y la gestión de Nx en un monorepo, ofrecen una ruta prometedora para construir aplicaciones frontend de gran escala. Requieren una inversión inicial en configuración y disciplina arquitectónica, pero los beneficios en escalabilidad y agilidad pueden ser enormes.
Este tutorial te ha proporcionado las herramientas y los conocimientos fundamentales para comenzar tu viaje en el mundo de los Micro Frontends con Angular. ¡Ahora es tu turno de construir el futuro de tus aplicaciones!
Tutoriales relacionados
- Optimización de Rendimiento en Aplicaciones Angular: Estrategias Avanzadas para una Experiencia Ultra Rápidaadvanced25 min
- ¡🚀 Despliega tu App Angular! Guía Completa de Build, Optimización y Publicación en Producciónintermediate18 min
- Componentes de Contenido Reutilizables en Angular: ¡Crea Módulos Compartidos y Bibliotecas!intermediate20 min
- Gestión de Estado Reactiva con NgRx en Angular: Una Guía Completaintermediate15 min
Comentarios (0)
Aún no hay comentarios. ¡Sé el primero!