Microfrontend
🧩 ¿Qué es un Microfrontend?
Es una arquitectura donde una aplicación frontend grande se divide en múltiples aplicaciones más pequeñas e independientes (microfrontends), cada una manejada por equipos diferentes, pero que juntas forman una experiencia unificada.
⚙️ ¿Qué es Webpack Module Federation?
Es una funcionalidad de Webpack (desde la versión 5) que permite que diferentes aplicaciones compartan módulos entre sí en tiempo de ejecución, como si fueran parte de la misma aplicación.
🏠 Host y 🌐 Remotos
🔹 Host (o shell)
Es la aplicación principal que carga otras aplicaciones remotas y coordina su integración. No contiene toda la lógica, sino que importa módulos desde otros microfrontends.
🔹 Remotos (remotes)
Son otras aplicaciones independientes que exponen sus componentes, módulos o funciones para que el host (u otros remotos) los utilicen.
🔄 Comunicación entre ellos (simplificada)
- El host se configura para consumir los módulos que exportan los remotos.
- Los remotos se configuran para exponer sus módulos (por ejemplo, un componente en Angular, Vue o React).
- Todo esto ocurre en tiempo de ejecución, no es necesario empacar todo junto.
🛠️ Como crear y configurar un host & remotos (Microfrontend)
Inicialización de proyectos
Angular ofrece un conjunto de comandos que facilitan el desarrollo, entre los cuales podemos encontrar uno para la generación automática de proyectos. Es importante mencionar que estaremos trabajando en una arquitectura microfrontend. Partiendo de este punto, se muestra a continuación, la creación de la app que será nuestro host:
ng new host-app --routing --style=scss
Este comando desplegara una serie de opciones entre las cuales nos preguntara, si queremos habilitar Server-side Rendering (SSR)a lo cual responderemos que NO, ya que para la arquitectura microfrontend no es necesaria. Una vez seleccionadas las opciones se desplegará un listado de proceso en ejecución que tardará unos minutos en concluir.

Posteriormente entraremos a la carpeta creada:
cd host-app
Luego, instalamos y configuramos Webpack Module Federation que se encargará de crear los archivos de configuración necesarios y de modificar los archivos main.ts y bootstrap.ts para habilitar la carga dinámica de módulos:
ng add @angular-architects/module-federation --project host-app --type host --port 4200
Una vez que concluye el proceso, se generará un directorio con el nombre de la aplicación, este contiene todos los componentes necesarios para desplegar una aplicación demo.

Luego crearemos la app "remote1”, que será nuestro "remote" dentro de nuestro proyecto de microfrontend. Para ello ejecutaremos el siguiente comando:
ng new remote1 --routing --style=scss
Después accedemos a la carpeta creada, con el siguiente comando:
cd remote1
Y posteriormente, instalamos y configuramos Webpack Module Federation que se encargará de crear los archivos de configuración necesarios y de modificar los archivos main.ts y bootstrap.ts para habilitar la carga dinámica de módulos:
ng add @angular-architects/module-federation@19.0.3 --project remote1 --type remote --port 4201
Una vez que concluye el proceso, se generará un directorio con el nombre de "remote1", este contiene todos los componentes necesarios para desplegar una aplicación demo.

Posteriormente crearemos un module llamado "sales" el cual expondremos a través del webpack module federation, para que pueda ser usado desde nuestro "host".
Para ello ejecutaremos los siguientes comandos, dentro de nuestro proyecto "remote1":
ng g c modules/sales/components/sales --standalone --skip-tests
Posteriormente crearemos manualmente el archivo: "sales.routes.ts" dentro de la carpeta "sales", con el siguiente contenido:

Una vez creado el archivo "sales.routes.ts", tendremos la siguiente estructura:

Es importante verificar que nuestro componente "sales.component.ts" tenga la inyección "standalone" y que tenga en los imports el CommonModule & RouterModule.

Después, actualizaremos el archivo "src/app/app.routes" de nuestro proyecto "remote1", donde cargaremos en la ruta "/sales" donde cargaremos las rutas de nuestro archivo "sales.routes.ts"

Y posteriormente, actualizaremos el archivo "webapack.config.js" de "remote1" para exponer nuestro módulo de sales, para que sea importado después por nuestro "host".

Es importante mencionar que se debe tener cuidado con el nombre con el que se exporta el modulo/componente ya que este debe tener el mismo nombre dentro de nuestro host, a la hora de importarlo. Y ademas el nombre debe ser escrito en PascalCase y debe estar en idioma ingles
Ejemplo:
- SalesComponent
- SalesListComponent
Y como pasos finales, abriremos el archivo "tsconfig.json" de "remote1" e incluiremos la siguiente configuración, en la llave "include".

Y por último, abriremos el archivo "app.routes" de nuestro proyecto de "host" y agregaremos el modulo "sales" que está dentro de nuestro proyecto "remote1":

Y ahora sí, podremos ejecutar nuestro microfrontend, para lo cual abriremos 2 terminales, para ejecutar los siguientes comandos:
- Dentro de la carpeta "host":
ng serve --port=4200
- Dentro de la carpeta "remote1":
ng serve --port=4201
Donde dentro de la ruta localhost:4200 se desplegará nuestra app "host"

Y en la ruta localhost:4200/sales se desplegará nuestra app "host" y además el módulo de "sales" de la app "remote1"

Y en la ruta de localhost:4201, se desplegará nuestra app "remote1"

Y por último en la ruta localhost:4201/sales, se desplegará nuestro modulo "sales".

Notas: en caso de tener problemas con el ruteo, bajar los servicios y volver a compilar.
Referencias:
| Tecnología | Referencias |
|---|---|
| Angular 19 | https://angular.dev/overview |
| Module Federation with Angular's Standalone Components | https://www.angulararchitects.io/blog/module-federation-with-angulars-standalone-components/ |
| Webpack Module Federation | https://webpack.js.org/concepts/module-federation/ |
🏗️ ¿Como importar remotos en el shell proporcionado por la area de arquitectura?
Para poder importar microfrontends remotos en nuestro host, a continuacion se muestran las reglas y los pasos a seguir:
🔹Inicialización del host
Al inicializar nuestro host, se inicia un proceso para poder obtener la configuración de los remotos a cargar a traves del metodo initializeFederation.
/* Archivo shell\src\app\app.config.ts */
/**
* Inicializa la configuración de federación de módulos (Native y Module Federation)
* @returns {() => Promise<void>} Función de inicialización
* @description
* 1. Carga la configuración remota
* 2. Inicializa Native Federation para módulos .json
* 3. Inicializa Module Federation para módulos .js
* 4. Genera rutas dinámicas
* 5. Actualiza la configuración del router
*/
const initializeFederation = () => {
const remoteRoutesService = inject(RemoteRoutesService);
const router = inject(Router);
return () => {
// 1. Carga la configuración remota
return remoteRoutesService
.getRemoteConfig()
.pipe(
// 2. Validar y cargar Native Federation (.json)
// eslint-disable-next-line @typescript-eslint/no-explicit-any
switchMap((apiRoutes: any) => {
const nativeConfig = remoteRoutesService.transformToFederationConfig(
apiRoutes.menuItems,
true
);
const hasNativeRemotes = Object.keys(nativeConfig.remotes).length > 0;
return hasNativeRemotes
? from(initNativeFederation(nativeConfig.remotes)).pipe(
map(() => apiRoutes),
catchError((err) => {
console.error("Error in initNativeFederation:", err);
return of(apiRoutes);
})
)
: of(apiRoutes);
}),
// 3. Validar y cargar Module Federation (.js)
switchMap((apiRoutes) => {
const moduleConfig = remoteRoutesService.transformToFederationConfig(
apiRoutes.menuItems,
false
);
const hasModuleRemotes = moduleConfig.remotes.length > 0;
if (hasModuleRemotes) {
/**
* Paso 3.1: Obtiene metadatos sobre las librerías compartidas via Native Federation
* @step Obtención de dependencias compartidas
* @type {SharedMappings}
*/
const shared = getShared();
initModuleFederation({
name: "shell",
remotes: moduleConfig.remotes,
shared,
});
}
return of(apiRoutes);
}),
// 4. Generar rutas dinámicas
switchMap((apiRoutes) => from(generateDynamicRoutes(apiRoutes))),
// 5. Actualizar rutas
tap((dynamicRoutes) => {
router.resetConfig([
...routes,
...dynamicRoutes,
{ path: "**", redirectTo: "/dashboard" },
]);
}),
// Manejo de errores
catchError((error) => {
console.error("Error inicializando federación:", error);
router.resetConfig(routes);
return of(null);
})
)
.toPromise();
};
};
/**
* Configuración principal de la aplicación Angular.
*
* @constant {ApplicationConfig} appConfig
* @property {Array<Provider>} providers - Proveedores de la aplicación
*
* @description
* Configuración incluye:
* - Optimización de detección de cambios con Zone.js
* - Proveedor HttpClient para peticiones HTTP
* - Router con rutas iniciales (sobrescritas después de inicialización)
* - Inicializador APP_INITIALIZER para carga asíncrona de rutas
*/
export const appConfig: ApplicationConfig = {
providers: [
provideZoneChangeDetection({ eventCoalescing: true }),
provideHttpClient(),
importProvidersFrom(HttpClientModule),
provideRouter(FALLBACK_ROUTES),
{
provide: APP_INITIALIZER,
useFactory: initializeAppFactory,
multi: true,
deps: [RemoteRoutesService, Router],
},
],
};
🔹Llamado a la API de Configuración de los Remotos
Posteriormente se invoca un servicio de una API para obtener las rutas remotas usando el servicio RemoteRoutesService especificamente en el metodo getRemoteConfig
/**
* Obtiene la configuración de menú desde el archivo JSON
* @returns {Observable<{menuItems: RemoteRoutesMenu[]}|null>} Observable con la configuración
* @description
* - Realiza la petición solo la primera vez y cachea los resultados
* - Usa shareReplay para multicasting
* - Actualiza la propiedad remoteRoutesMenu con los datos obtenidos
*/
getRemoteConfig(): Observable<{ menuItems: RemoteRoutesMenu[] } | null> {
if (!this.cachedRemotes$) {
this.cachedRemotes$ = this.http
.get<{
menuItems: RemoteRoutesMenu[];
}>('../../../assets/data/menu-config.json')
.pipe(
shareReplay(1),
tap((t) => (this.remoteRoutesMenu = t.menuItems))
);
// Configuración para Native Federation (.json)
this.nativeFederationConfig$ = this.cachedRemotes$.pipe(
// eslint-disable-next-line @typescript-eslint/no-explicit-any
map((config: any) =>
this.transformToFederationConfig(config.menuItems, true)
)
);
// Configuración para Module Federation (.js)
this.moduleFederationConfig$ = this.cachedRemotes$.pipe(
// eslint-disable-next-line @typescript-eslint/no-explicit-any
map((config: any) =>
this.transformToFederationConfig(config.menuItems, false)
)
);
}
return this.cachedRemotes$;
}
La estructura del archivo menu.config.json, debera ser igual a esta:
{
"menuItems": [
{
"title": "Entrega NF",
"icon": "airplay",
"type": "sub",
"active": true,
"remoteEntry": "http://localhost:4201/remoteEntry.json",
"remoteName": "entrega",
"children": [
{
"path": "entrega/producto",
"title": "Producto",
"type": "link",
"exposedModule": "./EntregaProducto"
}
]
},
{
"title": "Animals MF",
"icon": "list",
"type": "sub",
"active": false,
"remoteEntry": "http://localhost:4202/remoteEntry.js",
"remoteName": "remote2",
"children": [
{
"path": "animals/list",
"title": "Animals List",
"type": "link",
"exposedModule": "./AnimalsComponent"
}
]
}
]
}
A continuacion se muestra la interfaz para el archivo menu-config.json
/**
* Configuración para módulos remotos (Micro Frontends)
*/
export interface RemoteRoutesMenu {
path: string; // Nombre de la ruta
title: string; // Nombre del item
icon: string; // Nombre del icono a mostrar en el item del menu
type: string; // Tipo de item 'sub' | 'link' | 'extLink' | 'extTabLink'
badgeType?: string; // Opcional
badgeValue?: string; // Opcional
active: boolean; // Item del menu que estara desplegado por default
children?: RemoteRoutesMenu[]; // Opcional, Array de los hijos que tenga el item del menu
// Propiedades para Module Federation
remoteEntry?: string; // Opcional, Ruta del remoto
exposedModule: string; // Opcional, Ruta del módulo/componente expuesto
remoteName: string; // Nombre del remoto
}
🧩Ejemplo real para cargar un modulo remoto
A continuacion se explica como cargar su propio modulo remoto, en el shell implemetado por la area de arquitectura:
Supongamos que su modulo remoto tiene la siguiente informacion en el archivo webpack.config.js o en su defecto federation.config.js
/**
* Exporta la configuración de Webpack utilizando `withModuleFederationPlugin`.
*
* @property {string} name - Nombre del microfrontend remoto.
* @property {object} exposes - Módulos que este remoto expone a otros.
* @property {object} remotes - Definición del host y su URL de entrada remota (`remoteEntry.js`).
* @property {object} shared - Librerías compartidas entre el host y los remotos.
*/
module.exports = withModuleFederationPlugin({
name: 'productos'
filename: 'remoteEntry.js',
exposes: {
'./ProductListComponent':
'./src/app/modules/products/views/list/product-list.component.ts',
'./ProductCreateComponent':
'./src/app/modules/products/views/create/product-create.component.ts',
},
shared: {
...shareAll({
singleton: true,
strictVersion: true,
requiredVersion: 'auto',
}),
},
});
Entonces, siguiendo la informacion antes mencionada la estructura de nuestro menu-config.json debera ser la siguiente:
{
"menuItems": [
{
"title": "Productos",
"icon": "airplay",
"type": "sub",
"active": true,
"remoteEntry": "http://localhost:4201/remoteEntry.json",
"remoteName": "productos",
"children": [
{
"path": "producto/lista",
"title": "Producto Lista",
"type": "link",
"exposedModule": "./ProductListComponent"
},
{
"path": "producto/create",
"title": "Producto Create",
"type": "link",
"exposedModule": "./ProductCreateComponent"
}
]
}
]
}
Siguiendo las instrucciones anteriores, usted podra visualizar sus modulos remotos al iniciar sesion en la app shell.