ng-content

ng-content

Module Federation con Standalone Components de Angular

Photo by Raul Baz on Unsplash

Module Federation con Standalone Components de Angular

Dany Paredes's photo
Dany Paredes
·Jul 28, 2022·

7 min read

Subscribe to our newsletter and never miss any upcoming articles

Play this article

Table of contents

  • Configuraciones de router vs. Standalone Components
  • Situación inicial: Nuestro Micro Frontend
  • Activando Module Federation
  • Shell estático
  • Alternativa: Usando Shell dinámico
  • Bonus: Carga Usando Codigo
  • ¿Qué es lo siguiente?

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

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:

El ejemplo fue actualizado para usar los Standalone Components

📂 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 expondra 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.

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 Estatico. 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 estatico, también necesitamos definirt 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 dinamia, 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 programáticamente. 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 libron (En Ingles) , donde se responden todas estas preguntas y más: ebook gratuito

 
Share this