El conocimiento es el nuevo dinero.
Aprender es la nueva manera en la que inviertes
Acceso Cursos

Construyendo un Workflow Designer basado en plugins con Angular y Module Federation

· 5 min de lectura
Construyendo un Workflow Designer basado en plugins con Angular y Module Federation

En el artículo anterior de esta serie, mostré cómo utilizar Dynamic Module Federation. Esto nos permite cargar Microfrontends - o remotos, que es el término más general en Module Federation - no conocidos en tiempo de compilación. Ni siquiera necesitamos saber el número de Microfrontends por adelantado.

Mientras que el artículo anterior aprovechaba el router para integrar los Microfrontends o remotes disponibles, este artículo muestra cómo cargar componentes individuales. El ejemplo utilizado para esto es un simple diseñador de workflow basado en plugins.

Traducción en español del artículo original de Manfred Steyer "Building A Plugin-based Workflow Designer With Angular and Module Federation" actualizado el 10-06-2021

El diseñador de workflow actúa como un llamado host que carga las tareas de los plugins proporcionados como remotes. Así, pueden ser compilados y desplegados individualmente. Después de iniciar el workflow designer este obtiene una configuración que describe los plugins disponibles:

configuracion

Tenga en cuenta que estos plugins se proporcionan a través de diferentes orígenes (http://localhost:4201 y http://localhost:4202), y el diseñador de flujo de trabajo se sirve de un origen propio (http://localhost:4200).

📂 Código fuente

Gracias a Zack Jackson y Jack Herrington, que me ayudaron a entender el rater nuevo API para la Federación de Módulos Dinámicos.
Importante: Este artículo está escrito para Angular y Angular CLI 14.x y superior. Asegúrate de que tienes una versión adecuada si pruebas los ejemplos que aquí se describen.

Construyendo los plugins

Los plugins se proporcionan a través de aplicaciones Angular separadas. Para simplificar, todas las aplicaciones son parte del mismo monorepo. Su configuración de webpack utiliza Module Federation para exponer los plugins individuales como se muestra en los artículos anteriores de esta serie:

const { shareAll, withModuleFederationPlugin } = require('@angular-architects/module-federation/webpack');
 
 module.exports = withModuleFederationPlugin({
 
   name: 'mfe1',
 
   exposes: {
     './Download': './projects/mfe1/src/app/download.component.ts',
     './Upload': './projects/mfe1/src/app/upload.component.ts'
   },
 
   shared: {
     ...shareAll({ singleton: true, strictVersion: true, requiredVersion: 'auto' }),
   },
 
 });

Una diferencia con las configuraciones mostradas en los artículos anteriores es que aquí estamos exponiendo directamente componentes independientes. Cada componente representa una tarea que puede ser puesta en el flujo de trabajo.

La combinación de singleton: true y strictVersion: true hace que webpack emita un error en tiempo de ejecución cuando el shell y el/los micro frontend(s) necesitan diferentes versiones incompatibles (por ejemplo, dos versiones mayores diferentes). Si omitiéramos strictVersion o lo pusiéramos en falso, webpack solamente emitiría una advertencia en tiempo de ejecución.

Cargar los Plugins en el Workflow Designer

Para cargar los plugins en el workflow designer, estoy utilizando la función de ayuda loadRemoteModule proporcionada por el plugin @angular-architects/module-federation. Para cargar usamos loadRemoteModule de esta manera

import { loadRemoteModule } from '@angular-architects/module-federation';
 
 [...]
 
 const component = await loadRemoteModule({
     type: 'module',
     remoteEntry: 'http://localhost:4201/remoteEntry.js',
     exposedModule: './Download'
 })

Proporcionar metadatos sobre los plugins

En tiempo de ejecución, necesitamos proporcionar al Workflow Designer los datos clave sobre los plugins. El tipo utilizado para esto se llama PluginOptions y extiende el LoadRemoteModuleOptions mostrado en la sección anterior por un displayName y un componentName:

export type PluginOptions = LoadRemoteModuleOptions & {
     displayName: string;
     componentName: string;
 };
Una alternativa a esto es extender el Manifiesto de la Module Federation como se muestra en el artículo anterior sobre Module Federation Dinamico.

Mientras que el displayName es el nombre que se presenta al usuario, el componentName se refiere a la clase TypeScript que representa el componente Angular en cuestión.

Para cargar estos datos clave, el workflow designer aprovecha un LookupService:

@Injectable({ providedIn: 'root' })
 export class LookupService {
     lookup(): Promise<PluginOptions[]> {
         return Promise.resolve([
             {
                 type: 'module',
                 remoteEntry: 'http://localhost:4201/remoteEntry.js',
                 exposedModule: './Download',
 
                 displayName: 'Download',
                 componentName: 'DownloadComponent'
             },
             [...]
         ] as PluginOptions[]);
     }
 }

En aras de la simplicidad, el LookupService proporciona algunas ya entradas definidas, pero en el mundo real, es muy probable que solicite estos datos desde un punto final HTTP respectivo.

Creando Dinámicamente el Componente del Plugin

El Workflow Designer representa los plugins con un PluginProxyComponent. Toma un objeto PluginOptions a través de una entrada, carga el plugin descrito a través de Module Federation Dinámico y muestra el componente del plugin en el placeHolder:

@Component({
     standalone: true,
     selector: 'plugin-proxy',
     template: `
         <ng-container #placeHolder></ng-container>
     `
 })
 export class PluginProxyComponent implements OnChanges {
     @ViewChild('placeHolder', { read: ViewContainerRef, static: true })
     viewContainer: ViewContainerRef;
 
     constructor() { }
 
     @Input() options: PluginOptions;
 
     async ngOnChanges() {
         this.viewContainer.clear();
 
         const Component = await loadRemoteModule(this.options)
             .then(m => m[this.options.componentName]);
 
         this.viewContainer.createComponent(Component);
     }
 }

En versiones anteriores a Angular 13, necesitábamos utilizar un ComponentFactoryResolver para obtener la fábrica del componente cargado:

// Before Angular 13, we needed to retrieve a ComponentFactory
 //
 // export class PluginProxyComponent implements OnChanges {
 //     @ViewChild('placeHolder', { read: ViewContainerRef, static: true })
 //     viewContainer: ViewContainerRef;
 
 //     constructor(
 //       private injector: Injector,
 //       private cfr: ComponentFactoryResolver) { }
 
 //     @Input() options: PluginOptions;
 
 //     async ngOnChanges() {
 //         this.viewContainer.clear();
 
 //         const component = await loadRemoteModule(this.options)
 //             .then(m => m[this.options.componentName]);
 
 //         const factory = this.cfr.resolveComponentFactory(component);
 
 //         this.viewContainer.createComponent(factory, null, this.injector);
 //     }
 // }

Conectando Todo

Ahora, es el momento de conectar las partes mencionadas anteriormente. Para ello, el AppComponent del workflow designer obtiene un array de plugins y de workflows. El primero representa las PluginOptions de los plugins disponibles y, por lo tanto, todas las tareas disponibles, mientras que el segundo describe las PluginOptions de las tareas seleccionadas en la configuracion:

@Component({ [...] })
 export class AppComponent implements OnInit {
 
   plugins: PluginOptions[] = [];
   workflow: PluginOptions[] = [];
   showConfig = false;
 
   constructor(
     private lookupService: LookupService) {
   }
 
   async ngOnInit(): Promise<void> {
     this.plugins = await this.lookupService.lookup();
   }
 
   add(plugin: PluginOptions): void {
     this.workflow.push(plugin);
   }
 
   toggle(): void {
     this.showConfig = !this.showConfig;
   }
 }

El AppComponent utiliza el LookupService inyectado para rellenar su array de plugins. Cuando se añade un plugin al workflow, el método add pone su objeto PluginOptions en la array del workflow.

Para mostrar el workflow, el diseñador simplemente itera todos los elementos en la array del workflow y crea un plugin-proxy para ellos:

<ng-container *ngFor="let p of workflow; let last = last">
     <plugin-proxy [options]="p"></plugin-proxy>
     <i *ngIf="!last" class="arrow right" style=""></i>
 </ng-container> 

Como se ha comentado anteriormente, el proxy carga el plugin (al menos, si no está ya cargado) y lo muestra.

Además, para renderizar el menu de tasks que se muestra a la izquierda, recorre todas las entradas del array de plugins. Para cada uno de ellos muestra un hipervínculo que llama al método add:

<div class="vertical-menu">
     <a href="#" class="active">Tasks</a>
     <a *ngFor="let p of plugins" (click)="add(p)">Add {{p.displayName}}</a>
 </div>

Conclusión

Si bien Module Federation es útil para implementar Micro Frontends, también puede utilizarse para establecer arquitecturas de complementos. Esto nos permite extender una solución existente por parte de terceros. También parece ser un buen ajuste para las aplicaciones SaaS, que necesitan ser adaptadas a las necesidades de diferentes clientes.

Plataforma de cursos gratis sobre programación