La mayoría de los tutoriales sobre Module Federation y Angular exponen Micro Frontends con  NgModules. Sin embargo, con la introducción de Standalone Components en  tendremos soluciones ligeras de Angular que ya no usan NgModules. Esto nos lleva a la pregunta: ¿Cómo utilizar Module Federation en un mundo sin NgModules?

Traducción en español del artículo original de Manfred Steyer Module Federation with Angular’s Standalone Components actualizado el 07.05.2021

Si tu quieres leer más post como esté y practicar tu ingles recuerda visitar mi blog.

Recuerda que si quieres aprender Angular te dejo link al curso.

En este artículo, doy las respuestas y veremos cómo exponer  rutas que apuntan a Standalone Components y como cargar un Componente Standalone de forma individual. Para ello, he actualizado mi ejemplo para que funcione completamente sin NgModules:

📂 código fuente (rama: standalone-solution).

Configuraciones de router vs. Standalone Components

En general, podríamos cargar directamente los Standalone Components a través de Module Federation, esto es perfecta para un sistema de plugins pero los Micro Frontends son normalmente son un poco mas grandes. Es habitual que representen todo un dominio de negocio que, en general, contiene varios casos de uso que pertenecen trabajando juntos.

Curiosamente, los Standalone Components  pueden ser agrupados implementandolos en el router y por lo tanto, podemos exponerlos usando lazy loading para esas rutas.

Situación inicial: Nuestro Micro Frontend

El Micro Frontend utilizado aquí es una simple aplicación Angular que arranca un Standalone Components:

    // projects/mfe1/src/main.ts
 import { environment } from './environments/environment';
 import { enableProdMode, importProvidersFrom } from '@angular/core';
 import { bootstrapApplication } from '@angular/platform-browser';
 import { AppComponent } from './app/app.component';
 import { RouterModule } from '@angular/router';
 import { MFE1_ROUTES } from './app/mfe1.routes';
 
 if (environment.production) {
   enableProdMode();
 }
 
 bootstrapApplication(AppComponent, {
   providers: [
     importProvidersFrom(RouterModule.forRoot(MFE1_ROUTES))
   ]
 });

Al arrancar, la aplicación esta registra su configuración de en el router MFE1_ROUTES a través de los proveedores de servicios. Esta configuración del router  apunta a varios Standalone Components:

 import { Routes } from '@angular/router';
 import { FlightSearchComponent } from './booking/flight-search/flight-search.component';
 import { PassengerSearchComponent } from './booking/passenger-search/passenger-search.component';
 import { HomeComponent } from './home/home.component';
 
 export const MFE1_ROUTES: Routes = [
     {
         path: '',
         component: HomeComponent,
         pathMatch: 'full'
     },
     {
         path: 'flight-search',
         component: FlightSearchComponent
     },
     {
         path: 'passenger-search',
         component: PassengerSearchComponent
     }
 ];

El importProvidersFrom es un puente entre el actual RouterModule y los standalone components, pero en las futuras versiones el router  expondrá una función para configurar los proveedores del mismo. De acuerdo con el CFP esta función se llamará configureRouter.

El shell utilizado aquí es una aplicación Angular ordinaria. Usando lazy loading, nosotros vamos a hacer que haga referencia al Micro Frontend en tiempo de ejecución.

Si tu quieres aprender más acerca de angular recuerda visitar el siguiente video

Activando Module Federation

Para empezar, vamos a instalar el plugin Module Federation y activar Module Federation para el Micro Frontend:

npm i @angular-architects/module-federation
 
 ng g @angular-architects/module-federation:init --project mfe1 --port 4201 --type remote

Este comando genera un webpack.config.js, pero  tenemos que modificar la sección exposes de la siguiente manera:

const { shareAll, withModuleFederationPlugin } = require("@angular-architects/module-federation/webpack");
 
 module.exports = withModuleFederationPlugin({
   name: "mfe1",
 
   exposes: {
     // Preferred way: expose corse-grained routes
     "./routes": "./projects/mfe1/src/app/mfe1.routes.ts",
 
     // Technically possible, but not preferred for Micro Frontends:
     // Exposing fine-grained components
     "./Component": "./projects/mfe1/src/app/my-tickets/my-tickets.component.ts",
   },
 
   shared: {
     ...shareAll({ singleton: true, strictVersion: true, requiredVersion: "auto" }),
   }
 
 });

Esta configuración expone tanto la configuración del router del Micro Frontend (que apunta a los Standalone Components) como un Componente Standalone.

Shell estático

Ahora, vamos a activar también Module Federation para el shell. En esta sección, me centraré en la Module Federation Estático. Esto significa, que vamos a mapear las rutas que apuntan a nuestros Micro Frontends en el webpack.config.js.

En la siguiente sección muestra cómo cambiar a Dynamic Federation, donde podemos definir los datos para cargar un Micro Frontend en tiempo de ejecución.

Para habilitar Module Federation para el shell, vamos a ejecutar este comando

    ng g @angular-architects/module-federation:init --project shell --port 4200 --type host

El webpack.config.js generado para el shell necesita apuntar al Micro Frontend:

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" }),
   }
 
 });

Como vamos a ir en estático, también necesitamos definir todas las rutas configuradas (módulos EcmaScript) que hagan referencia a Micro Frontends:

// projects/shell/src/decl.d.ts
 
 declare module 'mfe1/*';

Ahora, todo lo que se necesita es implementar lazy loading en el shell, apuntando a las rutas y al Componente Standalone expuesto por el Micro Frontend:

// projects/shell/src/app/app.routes.ts
 
 import { Routes } from '@angular/router';
 import { HomeComponent } from './home/home.component';
 import { NotFoundComponent } from './not-found/not-found.component';
 import { ProgrammaticLoadingComponent } from './programmatic-loading/programmatic-loading.component';
 
 export const APP_ROUTES: Routes = [
     {
       path: '',
       component: HomeComponent,
       pathMatch: 'full'
     },
 
     {
       path: 'booking',
       loadChildren: () => import('mfe1/routes').then(m => m.BOOKING_ROUTES)
     },
 
     {
       path: 'my-tickets',
       loadComponent: () => 
           import('mfe1/Component').then(m => m.MyTicketsComponent)
     },
 
     [...]
 
     {
       path: '**',
       component: NotFoundComponent
     }
 ];

Alternativa: Usando Shell dinámico

Ahora, pasemos al Module Federation de forma dinámica, esto significa que no queremos definir nuestro remoto por adelantado en el webpack.config.js de lashell. Por lo tanto, vamos a comentar la sección remote:

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" }),
   }
 
 });

Además, en la configuración del router en shell, tenemos que cambiar los imports dinámicos utilizados antes por las llamadas a loadRemoteModule:

 import { Routes } from '@angular/router';
 import { HomeComponent } from './home/home.component';
 import { NotFoundComponent } from './not-found/not-found.component';
 import { ProgrammaticLoadingComponent } from './programmatic-loading/programmatic-loading.component';
 import { loadRemoteModule } from '@angular-architects/module-federation';
 
 export const APP_ROUTES: Routes = [
     {
       path: '',
       component: HomeComponent,
       pathMatch: 'full'
     },
     {
       path: 'booking',
       loadChildren: () => 
         loadRemoteModule({
           type: 'module',
           remoteEntry: 'http://localhost:4201/remoteEntry.js',
           exposedModule: './routes'
         })
         .then(m => m.MFE1_ROUTES)
     },
     {
       path: 'my-tickets',
       loadComponent: () => 
         loadRemoteModule({
           type: 'module',
           remoteEntry: 'http://localhost:4201/remoteEntry.js',
           exposedModule: './Component'
         })
         .then(m => m.MyTicketsComponent)
     },
     [...]
     {
       path: '**',
       component: NotFoundComponent
     }
 ];

La función loadRemoteModule toma todos los datos que Module Federation necesita para cargar el remoto. Estos datos  son sólo varias string por lo que se pueden cargar desde literalmente cualquier lugar.

Bonus: Carga Usando Codigo


Mientras que la mayoría de las veces, cargaremos los Micro Frontends (remotos) a través del router, también podemos cargar los componentes expuestos programaticamente. Para ello, necesitamos un placeholder con una variable de plantilla para el componente en cuestión:

    <h1>Programmatic Loading</h1>
     
     <div>
         <button (click)="load()">Load!</button>
     </div>
     
     <div #placeHolder></div>

Utilizando el ViewContainer  a través del decorador ViewChild:

import { Component, OnInit, ViewChild, ViewContainerRef } from '@angular/core';

@Component({
  selector: 'app-programmatic-loading',
  standalone: true,
  templateUrl: './programmatic-loading.component.html',
  styleUrls: ['./programmatic-loading.component.css']
})
export class ProgrammaticLoadingComponent implements OnInit {

  @ViewChild('placeHolder', { read: ViewContainerRef })
  viewContainer!: ViewContainerRef;

  constructor() { }

  ngOnInit(): void {
  }

  async load(): Promise<void> {

      const m = await import('mfe1/Component');
      const ref = this.viewContainer.createComponent(m.MyTicketsComponent);
      // const compInstance = ref.instance;
      // compInstance.ngOnInit()
  }

}

Este ejemplo muestra una solución de Module Federation estatico, por lo tanto, se utiliza un import dinámico para obtener el Micro Frontend.

Después de importar el componente remoto, podemos instanciarlo usando el método createComponent del ViewContainer. La referencia devuelta (ref) apunta a la instancia del componente con su propiedad instance. La instancia permite interactuar con el componente, por ejemplo, para llamar a métodos, establecer propiedades o configurar manejadores de eventos.

Si quisiéramos cambiar a la Federación Dinámica, volveríamos a utilizar loadRemoteModule en lugar de la dinámica import:

async load(): Promise<void> {
 
     const m = await loadRemoteModule({
       type: 'module',
       remoteEntry: 'http://localhost:4201/remoteEntry.js',
       exposedModule: './Component'
     });
 
     const ref = this.viewContainer.createComponent(m.MyTicketsComponent);
     // const compInstance = ref.instance;
 }

¿Qué es lo siguiente?

Hasta ahora, hemos visto cómo descomponer un gran cliente en varios Micro Frontends que incluso pueden utilizar diferentes frameworks. Sin embargo, cuando se trata de frontends de escala empresarial, surgen varias preguntas adicionales:

  • ¿Según qué criterios podemos subdividir una aplicación enorme en subdominios?
  • ¿Cómo podemos reforzar el acoplamiento flexible?
  • ¿Cómo podemos asegurarnos de que la solución se puede mantener durante años o incluso décadas?
  • ¿Qué patrones probados deberíamos utilizar?
  • ¿Qué otras opciones de Micro Frontends son proporcionadas por Module Federation?
  • ¿Debemos ir con un monorepo o con varios?

Puedes leer mas en el libro (En Ingles) , donde se responden todas estas preguntas y más:

La importancia de aprender microfrontends no solo radica en angular, sino que busca la mejor integración en cuanto a equipos de trabajo, a simplificar las cosas.

Si me preguntas en opinión personal mi consideración es que para destacar si es importante aprender cosas nuevas y mantenerte en constante formación.

Si tu quieres leer más post como esté y practicar tu ingles recuerda visitar mi blog.