Host y remotes: exposes y remotes
Federar necesita solo dos papeles. Un remote publica módulos con exposes; un host los consume declarándolos en remotes con su URL de entrada. La carga es dinámica y los papeles no son fijos: una misma app expone a unos y consume de otros. Aquí se ve el contrato exacto y cómo la URL de un remote puede decidirse en runtime.
Federar necesita solo dos papeles y un contrato entre ellos. Un remote publica código con exposes: declara qué módulos ofrece al mundo. Un host consume ese código con remotes: declara qué remotes conoce y dónde encontrarlos. La carga es dinámica —el host va a buscar el módulo justo cuando lo necesita— y los papeles no son fijos: una misma aplicación puede exponer a unos y consumir de otros en la misma configuración.
- Entender los papeles de host y remote, y cómo una misma app puede desempeñar ambos.
- Dominar
exposescomo el catálogo público que un remote publica. - Dominar
remotescomo la declaración de lo que un host consume y su URL de entrada. - Distinguir remotes estáticos, fijados en config, de remotes dinámicos, registrados en runtime.
Dos papeles, un mismo mecanismo
En una arquitectura federada, cada aplicación adopta uno de dos papeles frente a cada pieza de código, o los dos a la vez. El remote es el productor: expone módulos para que otros los usen. El host es el consumidor: carga módulos que viven en remotes. La distinción es de rol, no de naturaleza: nada impide que una aplicación sea host de unos remotes y a la vez remote para otros. Esa bidireccionalidad es lo que permite topologías donde no hay un centro único, sino una malla de aplicaciones que se prestan código entre sí.
El contrato entre ambos papeles se declara en la configuración del plugin de federación, y tiene tres campos que gobiernan todo: exposes en el remote, remotes en el host, y shared en ambos —que veremos en la próxima lección—. Los dos primeros son el tema de hoy: la oferta y la demanda de un mercado de módulos que se cierra en runtime.
No pienses en dos clases de aplicación, sino en dos verbos. Una app expone cuando publica módulos y consume cuando carga los de otra. La app raíz de un producto suele consumir mucho y exponer poco; una librería de UI federada expone mucho y consume poco; y una app intermedia hace ambas cosas. La configuración refleja exactamente qué verbos ejerce cada una.
exposes: el catálogo que publica un remote
exposes es un mapa. La clave es la ruta pública con la que el mundo pedirá el módulo; el valor es la ruta local del archivo que decides ofrecer. Todo lo que no aparezca en exposes permanece privado: sigue siendo un detalle interno del remote que nadie fuera puede importar. Exponer un módulo es, por eso, un acto deliberado de diseño de API pública, con la misma responsabilidad que el campo exports de un package.json.
// Config del REMOTE: publica su catalogo bajo remoteEntry.js
new ModuleFederationPlugin({
name: "remoteUI",
filename: "remoteEntry.js",
exposes: {
"./Boton": "./src/componentes/Boton.tsx",
"./tema": "./src/tema/index.ts",
},
shared: { react: { singleton: true } },
});
El name y el filename importan: juntos determinan cómo te encontrarán. El name es el identificador global del contenedor, y el filename es el archivo de entrada que se servirá en la URL del remote. Un host que quiera el botón pedirá remoteUI y, dentro de él, la ruta ./Boton. Lo que expones es un contrato: si mañana renombras ./Boton, rompes a todos los hosts que lo cargaban, exactamente igual que borrar una exportación pública de una librería.
remotes: lo que un host declara consumir
remotes es el mapa espejo, del lado del host. La clave es el alias local con el que te referirás al remote en tu código; el valor combina el name del contenedor y la URL de su entrada. Con esa línea, el host aprende dónde vive el remote y cómo llamarlo.
// Config del HOST: declara a quien consume y donde esta
new ModuleFederationPlugin({
name: "host",
remotes: {
remoteUI: "remoteUI@https://cdn.example.com/remoteUI/remoteEntry.js",
},
shared: { react: { singleton: true } },
});
A partir de ahí, importar un módulo remoto se parece a importar uno local: combinas el alias y la ruta expuesta. El bundler reconoce que remoteUI no es un paquete de node_modules, sino un remote, y reescribe el import en la carga dinámica del contenedor que vimos en la lección anterior.
// alias del remote mas ruta expuesta = el especificador federado
const { default: Boton } = await import("remoteUI/Boton");
Conviene ver los tres nombres alineados, porque es donde se confunde todo el mundo: la clave de exposes en el remote, el alias de remotes en el host y el especificador que escribes al importar tienen que encajar como una llave en su cerradura.
En el remote, exposes |
En el host, remotes |
Al importar |
|---|---|---|
clave ./Boton |
alias remoteUI |
import("remoteUI/Boton") |
| define la ruta pública | define dónde vive | combina ambos |
flowchart LR subgraph Remote remoteUI E[exposes: ./Boton y ./tema] --> RE[remoteEntry.js publicado] end subgraph Host R[remotes: remoteUI en su URL] --> I[import de remoteUI/Boton] end I -->|carga en runtime| RE style E fill:#89b4fa,color:#11111b style RE fill:#f9e2af,color:#11111b style R fill:#cba6f7,color:#11111b style I fill:#a6e3a1,color:#11111b
Remotes dinámicos: decidir la URL en runtime
En la configuración anterior, la URL del remote está fijada en el build del host: es un remote estático. Funciona, pero acopla el host a direcciones concretas y obliga a recompilarlo si una URL cambia. La runtime 2.0 ofrece la alternativa: registrar remotes en runtime, con la URL calculada en el momento. Así el mismo build del host puede apuntar a remotes distintos según el entorno, el cliente o un experimento.
import { registerRemotes, loadRemote } from "@module-federation/runtime";
// La URL puede venir del entorno, de una API de configuracion o del tenant.
registerRemotes([
{ name: "remoteUI", entry: resolverEntradaSegunEntorno() },
]);
const { default: Boton } = await loadRemote("remoteUI/Boton");
Este dinamismo es una de las razones por las que la federación desbordó al bundler y se convirtió en una runtime independiente. Registrar remotes en caliente habilita catálogos de microfrontends que se descubren por API, despliegues canary donde un porcentaje de usuarios recibe la URL nueva, y entornos —desarrollo, staging, producción— que comparten un único artefacto y solo cambian a qué remotes apuntan.
Un remote expone JavaScript, no tipos, así que por defecto el host ve any al importar. El plugin de tipos (@module-federation/dts-plugin) resuelve esto: el remote genera los .d.ts de lo que expone y los publica junto al manifiesto, y el host los descarga para tipar sus importaciones federadas. Sin ese puente, pierdes en la frontera toda la seguridad de tipos que TypeScript te da dentro de cada app.
El par exposes y remotes parece un simple emparejamiento de rutas, pero es en realidad un contrato entre dos aplicaciones que nunca se compilan juntas, y ahí está toda su fuerza y todo su peligro. Cuando importas un módulo dentro de una misma app, el compilador verifica el contrato entero por ti: comprueba que la exportación existe, que el tipo encaja, que no te equivocaste de nombre; si algo no cuadra, el build falla y te enteras en tu propia máquina. En la frontera federada ese guardián desaparece. La clave de exposes del remote y el especificador del host son dos declaraciones hechas en dos repositorios distintos, construidas por dos pipelines distintos, desplegadas en dos momentos distintos, y nada las verifica en conjunto hasta que el usuario carga la página y la carga funciona o revienta. Renombrar una clave de exposes es, por eso, un cambio incompatible tan serio como borrar una exportación pública de una librería con miles de dependientes, con el agravante de que ningún build te avisará antes de que sea tarde. Esta es la razón por la que la federación madura invierte tanto en devolverle rigor a esa frontera: los tipos publicados junto al manifiesto reconstruyen la verificación estática que se perdió, el versionado de la URL del remote permite evolucionar sin romper a los consumidores viejos, y las pruebas de contrato ocupan el lugar que en un monolito ocupaba el compilador. Tratar exposes como una API pública versionada —y no como un detalle de configuración que se cambia a la ligera— es la disciplina que distingue una malla de microfrontends que se puede mantener de una que se cae cada vez que un equipo toca un nombre sin avisar a los demás.
- Expón dos módulos desde un remote con
exposesy confirma en suremoteEntry.jsomf-manifest.jsonque ambos aparecen listados. - Consúmelos desde un host declarando el remote en
remotesy alineando alias, clave y especificador. - Renombra una clave de
exposesen el remote sin tocar el host, redepliega y observa el fallo en runtime: el precio de romper el contrato. - Convierte tu remote estático en dinámico con
registerRemotes, tomando la URL de una variable de entorno. - Añade el plugin de tipos y comprueba que el host deja de ver
anyal importar del remote.