La arquitectura de microfrontends permite dividir una aplicación web monolítica en aplicaciones más pequeñas y manejables, cada una de las cuales puede ser desarrollada y desplegada de manera independiente. Angular, combinado con Module Federation de Webpack 5, facilita la implementación de microfrontends, permitiendo el intercambio dinámico de módulos entre diferentes aplicaciones.
A continuación, se presenta un ejemplo de la estructura del árbol de ficheros para un proyecto de microfrontends con Angular y Module Federation. Este ejemplo incluye una aplicación host y dos microfrontends.
/microfrontends-project
|-- /host-app
| |-- /src
| | |-- /app
| | | |-- app.module.ts
| | | |-- app.component.ts
| | | -- app.component.html
| | -- bootstrap.ts
| |-- webpack.config.js
| |-- webpack.prod.config.js
| -- angular.json
|-- /microfrontend1
| |-- /src
| | |-- /app
| | | |-- mf1.module.ts
| | | |-- mf1.component.ts
| | | -- mf1.component.html
| | -- bootstrap.ts
| |-- webpack.config.js
| |-- webpack.prod.config.js
| -- angular.json
|-- /microfrontend2
| |-- /src
| | |-- /app
| | | |-- mf2.module.ts
| | | |-- mf2.component.ts
| | | -- mf2.component.html
| | -- bootstrap.ts
| |-- webpack.config.js
| |-- webpack.prod.config.js
|-- angular.json// webpack.config.js
const ModuleFederationPlugin = require("webpack/lib/container/ModuleFederationPlugin");
module.exports = {
// ... otras configuraciones
plugins: [
new ModuleFederationPlugin({
name: "host",
remotes: {
mf1: "mf1@http://localhost:3001/remoteEntry.js",
mf2: "mf2@http://localhost:3002/remoteEntry.js",
},
shared: ["@angular/core", "@angular/common", "@angular/router"],
}),
],
};Microfrontend 1
// webpack.config.js
const ModuleFederationPlugin = require("webpack/lib/container/ModuleFederationPlugin");
module.exports = {
// ... otras configuraciones
plugins: [
new ModuleFederationPlugin({
name: "mf1",
filename: "remoteEntry.js",
exposes: {
'./Module': './src/app/mf1.module.ts',
},
shared: ["@angular/core", "@angular/common", "@angular/router"],
}),
],
};Microfrontend 2
// webpack.config.js
const ModuleFederationPlugin = require("webpack/lib/container/ModuleFederationPlugin");
module.exports = {
// ... otras configuraciones
plugins: [
new ModuleFederationPlugin({
name: "mf2",
filename: "remoteEntry.js",
exposes: {
'./Module': './src/app/mf2.module.ts',
},
shared: ["@angular/core", "@angular/common", "@angular/router"],
}),
],
};Host
// app.module.ts
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { RouterModule } from '@angular/router';
import { AppComponent } from './app.component';
@NgModule({
declarations: [AppComponent],
imports: [
BrowserModule,
RouterModule.forRoot([
{
path: 'mf1',
loadChildren: () =>
import('mf1/Module').then((m) => m.Mf1Module),
},
{
path: 'mf2',
loadChildren: () =>
import('mf2/Module').then((m) => m.Mf2Module),
},
]),
],
providers: [],
bootstrap: [AppComponent],
})
export class AppModule {}Microfrontends
// mf1.module.ts
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { RouterModule } from '@angular/router';
import { Mf1Component } from './mf1.component';
@NgModule({
declarations: [Mf1Component],
imports: [
CommonModule,
RouterModule.forChild([
{
path: '',
component: Mf1Component,
},
]),
],
})
export class Mf1Module {}// mf2.module.ts
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { RouterModule } from '@angular/router';
import { Mf2Component } from './mf2.component';
@NgModule({
declarations: [Mf2Component],
imports: [
CommonModule,
RouterModule.forChild([
{
path: '',
component: Mf2Component,
},
]),
],
})
export class Mf2Module {}En una arquitectura de microfrontends, se pueden utilizar diferentes tipos de repositorios:
1. Monorepo: Un único repositorio que contiene el código del host y todos los microfrontends. Ventajas: fácil coordinación entre equipos, configuración compartida.
2. Multirepo: Repositorios separados para el host y cada microfrontend. Ventajas: mayor independencia, facilita la gestión de permisos y la escalabilidad del equipo.
Desarrollo Local Para ejecutar la configuración localmente, primero necesitas arrancar cada aplicación de microfrontend y luego el host. Supongamos que utilizamos scripts de npm para arrancar cada aplicación.
Microfrontend 1 En el package.json de microfrontend1, añadimos un script para arrancar la aplicación:
{
"scripts": {
"start": "ng serve --port 3001"
}
}Luego, en la terminal, ejecutamos:
cd microfrontend1
npm install
npm run startMicrofrontend 2 En el package.json de microfrontend2, añadimos un script para arrancar la aplicación:
{
"scripts": {
"start": "ng serve --port 3002"
}
}Luego, en la terminal, ejecutamos:
cd microfrontend2
npm install
npm run startHost Finalmente, en el package.json del host-app, añadimos un script para arrancar la aplicación:
{
"scripts": {
"start": "ng serve --port 3000"
}
}Luego, en la terminal, ejecutamos:
cd host-app
npm install
npm run startPara desplegar las aplicaciones en producción, necesitas construir cada microfrontend y el host, y luego alojar los archivos construidos en un servidor web.
Construcción de Microfrontends Para construir cada microfrontend, puedes utilizar el siguiente comando:
ng build --prodEsto generará los archivos construidos en el directorio dist. Asegúrate de configurar el outputPath en angular.json para especificar el directorio de salida.
Construcción del Host De manera similar, para el host:
ng build --prodServir los Archivos Construidos Puedes servir los archivos construidos utilizando cualquier servidor web estático, como Nginx, Apache, o incluso servicios de hosting como Firebase Hosting, AWS S3, o Netlify.
Ejemplo de Configuración de Nginx Aquí tienes un ejemplo de configuración de Nginx para servir las aplicaciones:
server {
listen 80;
server_name your-domain.com;
location / {
root /path/to/host-app/dist;
try_files $uri $uri/ /index.html;
}
location /mf1/ {
alias /path/to/microfrontend1/dist/;
try_files $uri $uri/ /index.html;
}
location /mf2/ {
alias /path/to/microfrontend2/dist/;
try_files $uri $uri/ /index.html;
}
}Para la comunicación entre microfrontends, puedes utilizar diferentes enfoques:
Servicios Compartidos: Crear servicios compartidos que utilicen observables para emitir y escuchar eventos. Event Bus: Implementar un Event Bus que permita la comunicación basada en eventos entre los microfrontends. Local Storage o Session Storage: Utilizar el almacenamiento local del navegador para compartir datos entre microfrontends.
Ejemplo de Servicio Compartido Un ejemplo simple de un servicio compartido podría ser:
shared.service.ts
import { Injectable } from '@angular/core';
import { Subject } from 'rxjs';
@Injectable({
providedIn: 'root',
})
export class SharedService {
private messageSource = new Subject<string>();
currentMessage = this.messageSource.asObservable();
constructor() {}
changeMessage(message: string) {
this.messageSource.next(message);
}
}Uso en Microfrontend 1
import { Component } from '@angular/core';
import { SharedService } from './shared.service';
@Component({
selector: 'app-mf1',
template: `<button (click)="sendMessage()">Send Message</button>`,
})
export class Mf1Component {
constructor(private sharedService: SharedService) {}
sendMessage() {
this.sharedService.changeMessage('Hello from Microfrontend 1');
}
}Uso en Microfrontend 2
import { Component, OnInit } from '@angular/core';
import { SharedService } from './shared.service';
@Component({
selector: 'app-mf2',
template: `<div>{{message}}</div>`,
})
export class Mf2Component implements OnInit {
message: string;
constructor(private sharedService: SharedService) {}
ngOnInit() {
this.sharedService.currentMessage.subscribe(
message => this.message = message
);
}
}