paint-brush
Cómo implementar una arquitectura Micro Frontend basada en Angularpor@tetianastoyko
24,891 lecturas
24,891 lecturas

Cómo implementar una arquitectura Micro Frontend basada en Angular

por Tetiana Stoyko2021/10/21
Read on Terminal Reader
Read this story w/o Javascript

Demasiado Largo; Para Leer

Micro Frontend es un concepto que considera un sitio web o una aplicación web como una colección de características controladas por equipos separados. Cada equipo está dedicado y especializado en un área específica de negocio o propósito. La arquitectura micro frontend es independiente de la tecnología. Los módulos más pequeños son más fáciles de aprender y comprender para los nuevos desarrolladores que ingresan a los equipos que una arquitectura monolítica con una estructura de código enorme. En nuestro artículo, cada parte es una aplicación web Angular separada y debe implementarse de forma independiente. Aquí describimos paso a paso cómo construir una micro interfaz basada en Angular.

Coin Mentioned

Mention Thumbnail
featured image - Cómo implementar una arquitectura Micro Frontend basada en Angular
Tetiana Stoyko HackerNoon profile picture

En esta época digital, la forma de una aplicación web es cada vez más grande y más sofisticada, por lo que con frecuencia deben ser manejadas por varios equipos. Su aplicación web puede tener funciones desarrolladas por diferentes equipos y, a veces, es mejor lanzar solo ciertas funciones en producción antes de entregar la aplicación web completa.


La mayoría de estas aplicaciones sofisticadas residen en el lado del cliente, lo que las hace más difíciles de mantener. Con una gran aplicación web monolítica, también hay otros problemas. Sin embargo, a medida que las aplicaciones se vuelven más complejas con el tiempo y requieren escalabilidad sobre la marcha y alta capacidad de respuesta, un diseño de micro-frontend basado en componentes Angular se vuelve cada vez más efectivo para abordar estos requisitos.


Micro Frontend es un concepto que considera un sitio web o una aplicación web como una colección de características controladas por equipos separados.


Cada equipo está dedicado y especializado en un área específica de negocio o propósito. Este equipo multifuncional crea funcionalidad de arriba a abajo, desde el servidor hasta la interfaz de usuario.

Beneficios de la Arquitectura Micro Frontend

Automatización de la canalización de CI/CD : dado que cada aplicación se integra y se implementa de forma independiente, simplifica la canalización de CI/CD. Debido a que todas las funcionalidades están separadas, no tiene que preocuparse por todo el programa mientras introduce una nueva característica. Si hay un pequeño error con el código en un módulo, la canalización de CI/CD interrumpirá todo el proceso de compilación.


Flexibilidad del equipo : Numerosos equipos pueden agregar valor a múltiples sistemas mientras trabajan por separado.


Responsabilidad única : este enfoque permite que cada equipo construya componentes con una responsabilidad única. Cada equipo de Micro Frontend se enfoca 100% en la funcionalidad de su Micro Frontend.


Reutilización : podrá usar el código en varios lugares. Un módulo creado y entregado puede ser reutilizado por varios equipos.


Agnosticismo tecnológico : la arquitectura Micro Frontend es independiente de la tecnología. Puede usar componentes de diferentes marcos de desarrollo web (React, Vue, Angular, etc.).


Aprendizaje simple : los módulos más pequeños son más fáciles de aprender y comprender para los nuevos desarrolladores que ingresan a los equipos que una arquitectura monolítica con una estructura de código enorme.

Empezando

Tenemos una arquitectura micro frontend que se muestra en la siguiente imagen:


Arquitectura de interfaz micro

En nuestro artículo, cada parte es una aplicación web Angular separada y debe implementarse de forma independiente.

Federación de módulo de encabezado y pie de página

Esta parte contiene al menos 2 componentes listos para exportar desde este módulo: en primer lugar, debemos crear una nueva aplicación y configurar un generador angular personalizado (este generador nos permite usar configuraciones personalizadas de paquetes web)


ng nuevo diseño npm i --save-dev ngx-build-plus


Ahora necesitamos crear archivos webpack.config.js y webpack.prod.config.js en la raíz de nuestra aplicación.


 // webpack.config.js const webpack = require("webpack"); const ModuleFederationPlugin =require("webpack/lib/container/ModuleFederationPlugin"); module.exports = { output: { publicPath: "http://localhost:4205/", uniqueName: "layout", }, optimization: { runtimeChunk: false, }, plugins: [ new ModuleFederationPlugin({ name: "layout", library: { type: "var", name: "layout" }, filename: "remoteEntry.js", exposes: { Header: './src/app/modules/layout/header/header.component.ts', Footer: './src/app/modules/layout/footer/footer.component.ts' }, shared: { "@angular/core": { singleton: true, requiredVersion:'auto' }, "@angular/common": { singleton: true, requiredVersion:'auto' }, "@angular/router": { singleton: true, requiredVersion:'auto' }, }, }), ], }; // webpack.prod.config.js module.exports = require("./webpack.config");


La federación de módulos nos permite compartir paquetes npm comunes entre diferentes interfaces, por lo que reducirá la carga útil de los chanks con carga diferida.


Podemos configurar la versión mínima requerida, se permiten dos o más versiones para un paquete, etc. Más detalles sobre las posibles opciones de complementos están aquí.


Tenemos una sección expuesta, por lo que aquí podemos definir qué elementos debemos permitir exportar desde nuestra aplicación de micro-frontend. En nuestro caso, exportamos solo 2 componentes.


Después de eso, debemos agregar un archivo de configuración personalizado en angular.json y cambiar el generador predeterminado a ngx-build-plus:


 { ... "projects": { "layout": { "projectType": "application", "schematics": { "@schematics/angular:component": { "style": "scss" }, "@schematics/angular:application": { "strict": true } }, "root": "", "sourceRoot": "src", "prefix": "app", "architect": { "build": { "builder": "ngx-build-plus:browser", "options": { "outputPath": "dist/layout", "index": "src/index.html", "main": "src/main.ts", "polyfills": "src/polyfills.ts", "tsConfig": "tsconfig.app.json", "inlineStyleLanguage": "scss", "assets": [ "src/favicon.ico", "src/assets" ], "styles": [ "src/styles.scss" ], "scripts": [], "extraWebpackConfig": "webpack.config.js" }, "configurations": { "production": { "budgets": [ { "type": "initial", "maximumWarning": "500kb", "maximumError": "1mb" }, { "type": "anyComponentStyle", "maximumWarning": "2kb", "maximumError": "4kb" } ], "extraWebpackConfig": "webpack.prod.config.js", "fileReplacements": [ { "replace": "src/environments/environment.ts", "with": "src/environments/environment.prod.ts" } ], "outputHashing": "all" }, "development": { "buildOptimizer": false, "optimization": false, "vendorChunk": true, "extractLicenses": false, "sourceMap": true, "namedChunks": true } }, "defaultConfiguration": "production" }, "serve": { "builder": "ngx-build-plus:dev-server", "configurations": { "production": { "browserTarget": "layout:build:production" }, "development": { "browserTarget": "layout:build:development", "extraWebpackConfig": "webpack.config.js", "port": 4205 } }, "defaultConfiguration": "development" }, "extract-i18n": { "builder": "@angular-devkit/build-angular:extract-i18n", "options": { "browserTarget": "layout:build" } }, "test": { "builder": "ngx-build-plus:karma", "options": { "main": "src/test.ts", "polyfills": "src/polyfills.ts", "tsConfig": "tsconfig.spec.json", "karmaConfig": "karma.conf.js", "inlineStyleLanguage": "scss", "assets": [ "src/favicon.ico", "src/assets" ], "styles": [ "src/styles.scss" ], "scripts": [], "extraWebpackConfig": "webpack.config.js" } } } } }, "defaultProject": "layout" }

Federación de módulo de página de registro

Esta aplicación web contendrá toda la lógica para la página de inicio de sesión/registro.


El flujo principal es casi el mismo, necesitamos crear una nueva aplicación e instalar un generador personalizado para usar configuraciones personalizadas de paquetes web.


ng nueva página de registro

npm i --save-dev ngx-build-plus


Después de eso, necesitamos crear webpack.config.js y webpack.prod.config.js

 // webpack.config.js const webpack = require("webpack"); const ModuleFederationPlugin = require("webpack/lib/container/ModuleFederationPlugin"); module.exports = { output: { publicPath: "http://localhost:4201/", uniqueName: "register", }, optimization: { runtimeChunk: false, }, plugins: [ new ModuleFederationPlugin({ name: "register", library: { type: "var", name: "register" }, filename: "remoteEntry.js", exposes: { RegisterPageModule: "./src/app/modules/register/register-page.module.ts", }, shared: { "@angular/core": { singleton: true, requiredVersion: 'auto' }, "@angular/common": { singleton: true, requiredVersion: 'auto' }, "@angular/router": { singleton: true, requiredVersion: 'auto' }, }, }), ], }; // webpack.prod.config.js module.exports = require("./webpack.config");


Como puede ver, aquí exportamos solo RegisterPageModule. Este módulo lo podemos usar como un módulo de carga diferida en nuestra aplicación de shell.


Además, debemos cambiar el generador predeterminado a ngx-build-plus y agregar configuraciones de paquete web en el archivo JSON angular (lo mismo que hicimos para el módulo Encabezado y pie de página antes).

Federación de módulos de panel

Este módulo presenta algunos datos para un usuario autorizado. El mismo enfoque que para la página de registro, pero con configuraciones de paquetes web personales:


 // webpack.config.js const webpack = require("webpack"); const ModuleFederationPlugin = require("webpack/lib/container/ModuleFederationPlugin"); module.exports = { output: { publicPath: "http://localhost:4204/", uniqueName: "dashboard", }, optimization: { runtimeChunk: false, }, plugins: [ new ModuleFederationPlugin({ name: "dashboard", library: { type: "var", name: "dashboard" }, filename: "remoteEntry.js", exposes: { DashboardModule: "./src/app/modules/dashboard/dashboard.module.ts", }, shared: { "@angular/core": { singleton: true, requiredVersion:'auto' }, "@angular/common": { singleton: true, requiredVersion:'auto' }, "@angular/router": { singleton: true, requiredVersion:'auto' }, }, }), ], };

Federación de módulos de aplicaciones de Shell

Aplicación principal que carga todos los módulos micro frontend separados en una sola aplicación. Como antes, creamos una nueva aplicación con un generador angular personalizado:


ng nuevo caparazón

npm i --save-dev ngx-build-plus


Agregue configuraciones personalizadas de paquetes web:

 // webpack.config.js const webpack = require("webpack"); const ModuleFederationPlugin = require("webpack/lib/container/ModuleFederationPlugin"); module.exports = { output: { publicPath: "http://localhost:4200/", uniqueName: "shell", }, optimization: { runtimeChunk: false, }, plugins: [ new ModuleFederationPlugin({ shared: { "@angular/core": { eager: true, singleton: true }, "@angular/common": { eager: true, singleton: true }, "@angular/router": { eager: true, singleton: true }, }, }), ], };


Pero antes necesitamos configurar la configuración del paquete web con el generador personalizado en el archivo angular.json.

En environment/environment.ts declaramos todas las configuraciones de módulos (para la versión prod necesitamos reemplazar la dirección localhost por la dirección pública implementada):


 export const environment = { production: false, microfrontends: { dashboard: { remoteEntry: 'http://localhost:4204/remoteEntry.js', remoteName: 'dashboard', exposedModule: ['DashboardModule'], }, layout: { remoteEntry: 'http://localhost:4205/remoteEntry.js', remoteName: 'layout', exposedModule: ['Header', 'Footer'], } } };


Luego, debemos agregar un panel de carga y una página de registro donde sea necesario. En primer lugar, necesitamos crear utilidades para la federación de módulos, donde podemos cargar módulos remotos desde otras aplicaciones.


 // src/app/utils/federation-utils.ts type Scope = unknown; type Factory = () => any; interface Container { init(shareScope: Scope): void; get(module: string): Factory; } declare const __webpack_init_sharing__: (shareScope: string) => Promise<void>; declare const __webpack_share_scopes__: { default: Scope }; const moduleMap: Record<string, boolean> = {}; function loadRemoteEntry(remoteEntry: string): Promise<void> { return new Promise<void>((resolve, reject) => { if (moduleMap[remoteEntry]) { return resolve(); } const script = document.createElement('script'); script.src = remoteEntry; script.onerror = reject; script.onload = () => { moduleMap[remoteEntry] = true; resolve(); // window is the global namespace }; document.body.append(script); }); } async function lookupExposedModule<T>( remoteName: string, exposedModule: string ): Promise<T> { // Initializes the share scope. This fills it with known provided modules from this build and all remotes await __webpack_init_sharing__('default'); const container = window[remoteName] as Container; // Initialize the container, it may provide shared modules await container.init(__webpack_share_scopes__.default); const factory = await container.get(exposedModule); const Module = factory(); return Module as T; } export interface LoadRemoteModuleOptions { remoteEntry: string; remoteName: string; exposedModule: string; } export async function loadRemoteModule<T = any>( options: LoadRemoteModuleOptions ): Promise<T> { await loadRemoteEntry(options.remoteEntry); return lookupExposedModule<T>( options.remoteName, options.exposedModule ); }


Y utilidades para construir rutas con carga diferida:


 // src/app/utils/route-utils.ts import { loadRemoteModule } from './federation-utils'; import { Routes } from '@angular/router'; import { APP_ROUTES } from '../app.routes'; import { Microfrontend } from '../core/services/microfrontends/microfrontend.types'; export function buildRoutes(options: Microfrontend[]): Routes { const lazyRoutes: Routes = options.map((o) => ({ path: o.routePath, loadChildren: () => loadRemoteModule(o).then((m) => m[o.ngModuleName]), canActivate: o.canActivate, pathMatch: 'full' })); return [ ...APP_ROUTES, ...lazyRoutes ]; }


Luego necesitamos definir un servicio de micro frontend:


 // src/app/core/services/microfrontends/microfrontend.service.ts import { Injectable } from '@angular/core'; import { Router } from '@angular/router'; import { MICROFRONTEND_ROUTES } from 'src/app/app.routes'; import { buildRoutes } from 'src/app/utils/route-utils'; @Injectable({ providedIn: 'root' }) export class MicrofrontendService { constructor(private router: Router) {} /* * Initialize is called on app startup to load the initial list of * remote microfrontends and configure them within the router */ initialise(): Promise<void> { return new Promise<void>((resolve) => { this.router.resetConfig(buildRoutes(MICROFRONTEND_ROUTES)); return resolve(); }); } }


Y archivo para el tipo:


 // src/app/core/services/microfrontends/microfrontend.types.ts import { LoadRemoteModuleOptions } from "src/app/utils/federation-utils"; export type Microfrontend = LoadRemoteModuleOptions & { displayName: string; routePath: string; ngModuleName: string; canActivate?: any[] };


Luego, debemos declarar los módulos remotos de acuerdo con las rutas:


 // src/app/app.routes.ts import { Routes } from '@angular/router'; import { LoggedOnlyGuard } from './core/guards/logged-only.guard'; import { UnloggedOnlyGuard } from './core/guards/unlogged-only.guard'; import { Microfrontend } from './core/services/microfrontends/microfrontend.types'; import { environment } from 'src/environments/environment'; export const APP_ROUTES: Routes = []; export const MICROFRONTEND_ROUTES: Microfrontend[] = [ { ...environment.microfrontends.dashboard, exposedModule: environment.microfrontends.dashboard.exposedModule[0], // For Routing, enabling us to ngFor over the microfrontends and dynamically create links for the routes displayName: 'Dashboard', routePath: '', ngModuleName: 'DashboardModule', canActivate: [LoggedOnlyGuard] }, { ...environment.microfrontends.registerPage, exposedModule: environment.microfrontends.registerPage.exposedModule[0], displayName: 'Register', routePath: 'signup', ngModuleName: 'RegisterPageModule', canActivate: [UnloggedOnlyGuard] } ]


Y use nuestro servicio Micro Frontend en el módulo principal de la aplicación:


 // src/app/app.module.ts import { APP_INITIALIZER, NgModule } from '@angular/core'; import { BrowserModule } from '@angular/platform-browser'; import { RouterModule } from '@angular/router'; import { AppRoutingModule } from './app-routing.module'; import { AppComponent } from './app.component'; import { APP_ROUTES } from './app.routes'; import { LoaderComponent } from './core/components/loader/loader.component'; import { NavbarComponent } from './core/components/navbar/navbar.component'; import { MicrofrontendService } from './core/services/microfrontends/microfrontend.service'; export function initializeApp( mfService: MicrofrontendService ): () => Promise<void> { return () => mfService.initialise(); } @NgModule({ declarations: [ AppComponent, NavbarComponent, LoaderComponent ], imports: [ BrowserModule, AppRoutingModule, RouterModule.forRoot(APP_ROUTES, { relativeLinkResolution: 'legacy' }), ], providers: [ MicrofrontendService, { provide: APP_INITIALIZER, useFactory: initializeApp, multi: true, deps: [MicrofrontendService], }, ], bootstrap: [AppComponent] }) export class AppModule { }


Ahora necesitamos cargar nuestros componentes de pie de página y encabezado. Para eso necesitamos actualizar el componente de la aplicación:

 // src/app/app.component.html <main> <header #header></header> <div class="content"> <app-navbar [isLogged]="auth.isLogged"></app-navbar> <div class="page-content"> <router-outlet *ngIf="!loadingRouteConfig else loading"></router-outlet> <ng-template #loading> <app-loader></app-loader> </ng-template> </div> </div> <footer #footer></footer> </main>


y el archivo src/app/app.component.ts se verá así:


 import { ViewContainerRef, Component, ComponentFactoryResolver, OnInit, AfterViewInit, Injector, ViewChild } from '@angular/core'; import { RouteConfigLoadEnd, RouteConfigLoadStart, Router } from '@angular/router'; import { loadRemoteModule } from './utils/federation-utils'; import { environment } from 'src/environments/environment'; @Component({ selector: 'app-root', templateUrl: './app.component.html', styleUrls: ['./app.component.scss'] }) export class AppComponent implements AfterViewInit, OnInit{ @ViewChild('header', { read: ViewContainerRef, static: true }) headerContainer!: ViewContainerRef; @ViewChild('footer', { read: ViewContainerRef, static: true }) footerContainer!: ViewContainerRef; loadingRouteConfig = false; constructor(private injector: Injector, private resolver: ComponentFactoryResolver, private router: Router ) {} ngOnInit() { this.router.events.subscribe(event => { if (event instanceof RouteConfigLoadStart) { this.loadingRouteConfig = true; } else if (event instanceof RouteConfigLoadEnd) { this.loadingRouteConfig = false; } }); } ngAfterViewInit(): void { // load header loadRemoteModule({ ...environment.microfrontends.layout, exposedModule: environment.microfrontends.layout.exposedModule[0], }) .then(module => { const factory = this.resolver.resolveComponentFactory(module.HeaderComponent); this.headerContainer?.createComponent(factory, undefined, this.injector); }); // load footer loadRemoteModule({ ...environment.microfrontends.layout, exposedModule: environment.microfrontends.layout.exposedModule[1], }) .then(module => { const factory = this.resolver.resolveComponentFactory(module.FooterComponent); this.footerContainer?.createComponent(factory, undefined, this.injector); }); } }


Aquí tenemos lógica para cargadores y lógica para componentes con carga diferida (encabezado, pie de página).

Comunicación entre Micro Frontends

Tenemos pocas formas de compartir datos entre diferentes micro frontends. Aquí se describen más detalles.


En nuestro caso, hemos decidido utilizar Custom Event para la comunicación. El evento personalizado nos permite enviar datos personalizados a través de la carga útil del evento.


Un módulo debería enviar eventos personalizados como este:


 const busEvent = new CustomEvent('app-event-bus', { bubbles: true, detail: { eventType: 'auth-register', customData: 'some data here' } }); dispatchEvent(busEvent);


Otros módulos pueden suscribirse a este evento:


 onEventHandler(e: CustomEvent) { if (e.detail.eventType === 'auth-register') { const isLogged = Boolean(localStorage.getItem('token')); this.auth.isLogged = isLogged; if (isLogged) { this.router.navigate(['/']); } else { this.router.navigate(['/signup']); } } } ngOnInit() { this.$eventBus = fromEvent<CustomEvent>(window, 'app-event-bus').subscribe((e) => this.onEventHandler(e)); // ... }

Vista previa de demostración

Para usuarios no autorizados:


Para usuarios autorizados:


Conclusión

Existe una demanda creciente de arquitecturas micro frontend más manejables a medida que las bases de código frontend se vuelven más sofisticadas con el tiempo. Por lo tanto, es crucial poder trazar límites claros que establezcan los niveles correctos de acoplamiento y cohesión entre las entidades técnicas y de dominio, así como escalar la entrega de software entre equipos independientes y autónomos.


Vea el ejemplo de código completo en el repositorio de GitHub.


También publicado aquí