Importante: Este artículo está escrito para Angular y Angular CLI 14.
En el artículo anterior, he mostrado cómo usar Module Federation, este es parte de webpack a partir de la versión 5, para implementar micro frontends. Este artículo muestra cómo crear un micro frontend basado en Angular utilizando el router para cargar un micro frontend compilado y desplegado por separado.
Antes de continuar avanzando, recuerda que si tu quieres aprender acerca de Micro-Frontend tenemos 🥳 WORKSHOP
El resultado es similar al del artículo anterior, pero usando Angular:
El Micro frontend cargado se muestra dentro del borde discontinuo rojo sin la shell:
Puede descargar el código desde Github
Usando Module Federation en proyectos de Angular
Nuestro ejemplo aquí asume que tanto el shell como el micro frontend son proyectos en el mismo espacio de trabajo de Angular. Para empezar, necesitamos decirle a la CLI que use Module Federation al momento de generarlos. Sin embargo, como la CLI nos simplifica la configuracion de webpack, nosotros necesitamos hacer en webpack de forma personalizada.
Para eso el paquete @angular-architects/module-federation proporciona esa interfaz media para interactuar y modificar el mismo. Para empezar, utilizarlo y agregarlo en el proyecto, podemos usar el comando ng add
:
ng add @angular-architects/module-federation --project shell --port 4200 --type host
ng add @angular-architects/module-federation --project mfe1 --port 4201 --type remote
Si usas Nx, debes instalar la librería por separado. Después de eso, puede utilizar el flag :init
npm i @angular-architects/module-federation -D
ng g @angular-architects/module-federation:init --project shell --port 4200 --type host
ng g @angular-architects/module-federation:init --project mfe1 --port 4201 --type remote
El argumento --type
fue añadido en la versión 14.3 y asegura que sólo se genere la configuración necesaria.
Aunque es obvio que el shell del proyecto contiene el código del shell, mfe1
significa Micro Frontend 1, el comando mostrado hace varias cosas:
- Generar una base de un
webpack.config.js
para usar Module Federation - Instalar un constructor personalizado haciendo que webpack dentro del CLI utilice el
webpack.config.js
generado. - Asigna un nuevo puerto para
ng serve
para que varios proyectos puedan ser servidos simultáneamente.
Ten en cuenta que el webpack.config.js
es sólo una configuración parcial de webpack
. Sólo contiene cosas para controlar module federation. El resto es generado por el CLI como siempre.
El Shell (Host)
Empecemos con el shell, que también se llama host en module federation, este utiliza el router para cargar de forma asyncronza el FlightModule
:
export const APP_ROUTES: Routes = [
{
path: '',
component: HomeComponent,
pathMatch: 'full'
},
{
path: 'flights',
loadChildren: () => import('mfe1/Module').then(m => m.FlightsModule)
},
];
Sin embargo, la ruta mfe1/Modul
e que se importa aquí, no existe dentro del shell. Es sólo una ruta virtual que apunta a otro proyecto.
Para facilitar el compilador de TypeScript, necesitamos una definicion .d.ts
para ello:
// decl.d.ts
declare module 'mfe1/Module';
Además, tenemos que decirle a webpack que todas las rutas que empiezan por mfe1
apuntan a otro proyecto. Esto se puede hacer en el webpack.config.js
generado:
const { shareAll, withModuleFederationPlugin } = require('@angular-architects/module-federation/webpack');
module.exports = withModuleFederationPlugin({
remotes: {
"mfe1": "http://localhost:4201/remoteEntry.js",
},
shared: {
...shareAll({ singleton: true, strictVersion: true, requiredVersion: 'auto' }),
},
});
La sección de remote asigna la ruta mfe1
al micro frontend a su entrada remota. Este es un pequeño archivo generado por webpack cuando se construye el remote, este es cargado en tiempo de ejecución para obtener toda la información necesaria para interactuar con el micro frontend.
Aunque especificar la URL de la entrada remota de esta manera es conveniente para el desarrollo, necesitamos un enfoque más dinámico para la producción.
El siguiente artículo de esta serie brindaremos una solución para esto: Dynamic Federation
La propiedad shared
define los paquetes npm a compartir entre el shell y el/los micro frontend(s). Para esta propiedad, la configuración utiliza la propiedad shareAll
que básicamente comparte todas las dependencias encontradas en su package.json
. Si bien esto ayuda a obtener rápidamente una configuración , podría conducir a demasiadas dependencias compartidas.
Mas adelante hablaremos sobre este tema.
La combinación de singleton: true
y strictVersion: true
hace que webpack emita un error de ejecución cuando el shell y los micro frontend(s) necesiten diferentes versiones incompatibles (por ejemplo, dos versiones mayores diferentes). Si omitiéramos strictVersion o lo pusiéramos en false, webpack sólo emitiría una advertencia en tiempo de ejecución.
Hablaremos en otro articulos de como gestinoar las versiones en esta serie.
La propiedad requiredVersion: 'auto'
es un pequeño extra proporcionado por el plugin @angular-architects/module-federation
. Este busca la versión utilizada en tu package.json y evita varios problemas.
Al asignar el valor auto en requiredVersion, este remplaza el valor 'auto' con la versión encontrada en tu package.json.
El Micro frontend (aka Remote)
El Micro frontend también llamado remote en Module Federation el cual es una aplicacion de Angular ordinaria. Tiene rutas definidas dentro del AppModule:
export const APP_ROUTES: Routes = [
{ path: '', component: HomeComponent, pathMatch: 'full'}
];
Además, el FlightsModule:
@NgModule({
imports: [
CommonModule,
RouterModule.forChild(FLIGHTS_ROUTES)
],
declarations: [
FlightsSearchComponent
]
})
export class FlightsModule { }
Este módulo tiene algunas rutas propias:
export const FLIGHTS_ROUTES: Routes = [
{
path: 'flights-search',
component: FlightsSearchComponent
}
];
Para que sea posible cargar el FlightsModule en el shell, también necesitamos exponerlo a través de la configuración de webpack del remote:
const { shareAll, withModuleFederationPlugin } = require('@angular-architects/module-federation/webpack');
module.exports = withModuleFederationPlugin({
name: 'mfe1',
exposes: {
'./Module': './projects/mfe1/src/app/flights/flights.module.ts',
},
shared: {
...shareAll({ singleton: true, strictVersion: true, requiredVersion: 'auto' }),
},
});
La configuración mostrada aquí expone el FlightsModule
bajo el nombre público Module. La sección shared
apunta a las librerias compartidas con el shell.
Iniciando el Micro frontend
Para probar todo, sólo tenemos que iniciar el shell y el micro frontend:
ng serve shell -o
ng serve mfe1 -o
Entonces, al hacer clic en Flights en el shell, se carga el micro frontend:
Sugerencia: También puedes usar el script npm run:all
este se instala con schematics ng-add y init:
npm run run:all
Para iniciar sólo algunas aplicaciones, añada sus nombres como argumentos:
npm run run:all shell mfe1
Mirando los detalles
Vale, eso ha funcionado bastante bien. ¿Pero has echado un vistazo a tu main.ts?
Se parece a esto:
import('./bootstrap')
.catch(err => console.error(err));
El código que normalmente se encuentra en el archivo main.ts
fue trasladado al archivo bootstrap.ts
. Todo esto fue hecho por el plugin @angular-architects/module-federation.
Aunque esto no parece tener mucho sentido a primera vista, es un patrón típico que se encuentra en las aplicaciones basadas en Module Federation. La razón es que Module Federation necesita decidir qué versión de una biblioteca compartida cargar.
Si el shell, por ejemplo, está usando la versión 12.0 y uno de los micro frontends ya está construido con la versión 12.1, decidirá cargar esta última.
Para buscar los metadatos necesarios para tomar esta decision, Module Fedaration extra las importaciones dinámicas como esta de aquí. A diferencia de las importaciones estáticas más tradicionales, las importaciones dinámicas son asíncronas.
Por lo tanto, Module Federation puede decidir sobre las versiones a utilizar y cargarlas realmente.
Más detalles: Compartir dependencias
Como se ha mencionado anteriormente, el uso de shareAll
permite una primera configuración rápida que "simplemente funciona". Sin embargo, puede conducir a demasiados bundles compartidos. Dado que las dependencias compartidas no pueden treeshaken
y por defecto terminan en bundles separados que necesitan ser cargados, es posible que desee optimizar este comportamiento cambiando de shareAll
a share
:
// Import share instead of shareAll:
const { share, withModuleFederationPlugin } = require('@angular-architects/module-federation/webpack');
module.exports = withModuleFederationPlugin({
// Explicitly share packages:
shared: share({
"@angular/core": { singleton: true, strictVersion: true, requiredVersion: 'auto' },
"@angular/common": { singleton: true, strictVersion: true, requiredVersion: 'auto' },
"@angular/common/http": { singleton: true, strictVersion: true, requiredVersion: 'auto' },
"@angular/router": { singleton: true, strictVersion: true, requiredVersion: 'auto' },
}),
});
Conclusión
La implementación de Micro frontend ha implicado hasta ahora numerosos trucos y soluciones. Module Federation de Webpack proporciona por fin una solución sencilla y sólida para ello. Para mejorar el rendimiento, las librerías que pueden ser compartidas y se pueden configurar estrategias para tratar con versiones incompatibles.
También es interesante que los micro frontend sean cargados por Webpack y sin referencias en el código fuente del host o del remoto. Esto simplifica el uso de la Module Federation y el código fuente resultante, que no requiera frameworks de micro frontend adicionales.
Sin embargo, este enfoque también pone más responsabilidad en los desarrolladores. Por ejemplo, hay que asegurarse de que los componentes que sólo se cargan en tiempo de ejecución y que aún no se conocían en el momento de la compilación también interactúen como se desea.
También hay que lidiar con posibles conflictos de versión. Por ejemplo, es probable que componentes que fueron compilados con versiones de Angular completamente diferentes no funcionen juntos en tiempo de ejecución. Estos casos deben ser evitados con convenciones o al menos reconocidos lo antes posible con pruebas de integración.
Traducción en español del artículo original de Manfred Steyer "The Microfrontend Revolution: Module Federation with Angular" actualizado el 06-06-2022