Crear una arquitectura de microservicio usando spa único (migración de un proyecto existente)

imagen



Este es el primer artículo sobre este tema, se planean un total de 3:



  1. * Cree una aplicación raíz a partir de su proyecto existente, agregue 3 microaplicaciones (vue, react, angular)
  2. Comunicación entre microaplicaciones
  3. Trabajando con git (implementación, actualizaciones)


Tabla de contenido



  1. una parte común
  2. Porque es necesario
  3. Cree un contenedor raíz (consulte la definición a continuación) a partir de su monolito
  4. Crea una microaplicación VUE (vue-app)
  5.  Crear una micro-aplicación REACT (react-app)
  6.  Crea una microaplicación ANGULAR (angular-app)


1. Parte general



El objetivo de este artículo es agregar la capacidad de usar un proyecto monolítico existente como contenedor raíz para una arquitectura de microservicio.



El proyecto existente se realiza en angular 9.



Para la arquitectura de microservicio, utilizamos la biblioteca de spa único .



Necesita agregar 3 proyectos al proyecto raíz, usamos diferentes tecnologías: vue-app, angular-app, react-app (ver p. 4, 5, 6).



Paralelamente a la creación de este artículo, estoy intentando implementar esta arquitectura en un proyecto de producción en el que estoy trabajando actualmente. Por tanto, intentaré describir todos los errores que tengo en el proceso de desarrollo y sus soluciones.



Aplicación root (en adelante root) - la raíz (contenedor) de nuestra aplicación. Pondremos (registraremos) todos nuestros microservicios en él. Si ya tiene algún proyecto y desea implementar esta arquitectura en él, entonces su proyecto existente será la aplicación raíz, desde donde con el tiempo intentará roer partes de su aplicación, crear microservicios separados y registrarlos en este contenedor.



Este enfoque de creación de un contenedor raíz brindará una excelente oportunidad para migrar a otra tecnología sin mucho dolor.



Por ejemplo, decidimos pasar de angular a vue por completo, pero el proyecto es audaz y, por el momento, aporta mucho dinero al negocio.



Sin la arquitectura de microservicios, esto no habría aparecido en la mente, solo para personas desesperadas que creen en los unicornios y que todos somos un holograma.

Para cambiar a una nueva tecnología en realidad, es necesario reescribir todo el proyecto, y solo así podríamos drogarnos por su apariencia en la batalla.



Otra opción es la arquitectura de microservicios. Puede crear un proyecto raíz desde su monolito, agregar un nuevo proyecto allí en el mismo vue, configurar el roaming en la raíz, ya está. Puede lanzarse a la batalla, cortar gradualmente pequeños trozos de la raíz del proyecto y transferirlos a su microproyecto vue. Esto deja solo los archivos en su contenedor raíz que son necesarios para importar su nuevo proyecto.



Esto se puede hacer aquí y ahora, sin pérdida de sangre y, lo más importante, es real.

Usaré angular como raíz, ya que el proyecto existente fue escrito en él.



La interfaz general en la que se incluirá la aplicación de una sola página:



bootstrap (mounter, bus) : se llama después de que se carga el servicio, le dirá qué elemento de la casa necesita montar, le dará un bus de mensajes al que se suscribirá el microservicio y podrá escuchar y enviar solicitudes y el comando de



montaje. () - montar la aplicación desde casa



unmount () - desmontar la aplicación



unload () - descargar la aplicación



En el código, describiré una vez más el funcionamiento de cada método localmente en el lugar de uso.



2. ¿Por qué es necesario?



Comencemos en este punto estrictamente en orden.



Hay 2 tipos de arquitectura:



  1. Monolito
  2. Arquitectura de microservicio


imagen



Con el monolito, todo es bastante simple y lo más familiar posible para todos nosotros. Fuerte cohesión, enormes bloques de código, repositorio compartido, toneladas de métodos.



Al principio, la arquitectura monolítica es lo más conveniente y rápida posible. No hay problemas ni dificultades para crear archivos de integración, capas intermedias, modelos de eventos, buses de datos, etc.



El problema aparece cuando su proyecto crece, aparece una gran cantidad de funciones complejas y separadas para diferentes propósitos. Toda esta funcionalidad comienza a estar ligada dentro del proyecto a algunos modelos generales, estados, utilidades, interfaces, métodos, etc.



Además, la cantidad de directorios y archivos en el proyecto se vuelve enorme con el tiempo, hay problemas para encontrar y entender el proyecto en su conjunto, se pierde la "vista superior", lo que da claridad a lo que estamos haciendo, dónde está lo que está y quién lo necesita.



Además de todo esto, la Ley de Eagleson está funcionando , que dice que su código, que no ha mirado durante 6 meses o más, parece que lo escribió otra persona.



Lo más doloroso es que todo crecerá exponencialmente, como resultado, comenzarán muletas, las cuales hay que sumar por la complejidad de mantener el código en conexión con lo anterior y, con el tiempo, las oleadas de términos irresponsables que se van produciendo.



Como resultado, si tiene un proyecto en vivo que está en constante evolución, se convertirá en un gran problema, el eterno descontento de su equipo, una gran cantidad de personas: horas para realizar cambios menores en el proyecto, un umbral de entrada bajo para nuevos empleados y mucho tiempo para implementar el proyecto en la batalla. Todo esto conduce al desorden, bueno, ¿amamos el orden?



¿Esto siempre sucede con un monolito?



¡Por supuesto no! Todo depende del tipo de proyecto, de los problemas que surjan durante el desarrollo del equipo. Es posible que su proyecto no sea tan grande, para realizar una tarea empresarial compleja, esto es normal y creo que es correcto.



En primer lugar, debemos prestar atención a los parámetros de nuestro proyecto. 



Intentaré sacar los puntos mediante los cuales puede comprender si realmente necesitamos una arquitectura de microservicio:



  • 2 o más equipos están trabajando en el proyecto, el número de desarrolladores front-end es 10+;
  • Su proyecto consta de 2 o más modelos comerciales, por ejemplo, tiene una tienda en línea con una gran cantidad de productos, filtros, notificaciones y la funcionalidad de distribución de entrega por mensajería (2 modelos separados, no pequeños, que interferirán entre sí). Todo esto puede vivir por separado y no depender unos de otros.
  • El conjunto de capacidades de la interfaz de usuario crece diaria o semanalmente sin afectar al resto del sistema.


Los micrófonos se utilizan para:



  • Se pueden desarrollar, probar e implementar partes separadas de la interfaz de forma independiente;
  • Se pueden agregar, quitar o reemplazar partes de la interfaz sin volver a ensamblar;
  •   .
  • , - «», - ( ) -.
  • ,
  • .


single-spa ?



  • (, React, Vue Angular) , .
  • Single-spa , , .
  • .


El microservicio, a mi entender, es una aplicación independiente de una sola página que resolverá solo una tarea de usuario. Esta aplicación tampoco tiene que resolver toda la tarea del equipo. 



SystemJS es una biblioteca JS de código abierto que se usa comúnmente como polyfill para navegadores.



El polyfill es un fragmento de código JS que se utiliza para proporcionar una funcionalidad moderna para navegadores antiguos que no lo admiten.



Una de las características de SystemJS es un mapa de importación, que le permite importar un módulo a través de la red y asignarlo a un nombre de variable.



Por ejemplo, puede usar un mapa de importación para una biblioteca de React que se carga a través de un CDN:



¡PERO!



Si estás creando un proyecto desde cero, incluso considerando que has determinado todos los parámetros de tu proyecto, has decidido que tendrás un mega súper proyecto enorme con un equipo de más de 30 personas, ¡espera!



Me gusta mucho la idea del conocido fundador de la idea de los microservicios: Martin Fowler .



Propuso combinar el enfoque monolítico y los microservicios en uno (MonolithFirst). Su idea principal es la siguiente: 

no debe comenzar un nuevo proyecto con microservicios, incluso si está completamente seguro de que la aplicación futura será lo suficientemente grande como para justificar este enfoque


También describiré las desventajas de usar una arquitectura de este tipo aquí:



  • La interacción entre fragmentos no se puede lograr con los métodos de tubo estándar (DI, por ejemplo).
  • ¿Qué pasa con las dependencias comunes? Después de todo, el tamaño de la aplicación crecerá a pasos agigantados, si no se eliminan de los fragmentos.
  • Alguien aún debe ser responsable del enrutamiento en la aplicación final.
  • No está claro qué hacer con el hecho de que diferentes microservicios pueden ubicarse en diferentes dominios.
  • Qué hacer si uno de los fragmentos no está disponible / no se puede procesar.


3. Creación de un contenedor raíz



Y entonces, suficiente teoría, es hora de empezar.



Ir a la consola



ng add single-spa-angular
npm i systemjs@6.1.4,
npm i -d @types/systemjs@6.1.0,
npm import-map-overrides@1.8.0


En ts.config.app.json, declaraciones de importación global (tipos)



// ts.config.app.json

"compilerOptions": {
    "outDir": "./out-tsc/app",
    "types": [
(+)     "systemjs"
    ]
},




Agregue a app-routing.module.ts todas las micro-aplicaciones que agreguemos a la raíz



// app-routing.module.ts

{
    path: 'vue-app',
    children: [
        {
            path: '**',
            loadChildren: ( ) => import('./spa-host/spa-host.module').then(m => m.SpaHostModule),
            data: { app: '@somename/vue-app' }
        }
    ]
},
{
    path: 'angular-app',
    children: [
        {
            path: '**',
            loadChildren: ( ) => import('./spa-host/spa-host.module').then(m => m.SpaHostModule),
            data: { app: '@somename/angular-app' }
        }
    ]
},
{
    path: 'react-app',
    children: [
        {
            path: '**',
            loadChildren: ( ) => import('./spa-host/spa-host.module').then(m => m.SpaHostModule),
            data: { app: '@somename/react-app' }
        }
    ]
},


También necesitas agregar config



// extra-webpack.config.json

module.exports = (angularWebpackConfig, options) => {
    return {
        ...angularWebpackConfig,
        module: {
            ...angularWebpackConfig.module,
            rules: [
                ...angularWebpackConfig.module.rules,
            {
                parser: {
                    system: false
                }
             }
           ]
        }
    };
}


Cambiemos el archivo package.json, agreguemos todo lo necesario para el trabajo o



// package.json

"dependencies": {
      ...,
(+) "single-spa": "^5.4.2",
(+) "single-spa-angular": "^4.2.0",
(+) "import-map-overrides": "^1.8.0",
(+) "systemjs": "^6.1.4",
}
"devDependencies": {
      ...,
(+)  "@angular-builders/custom-webpack": "^9",
(+)  "@types/systemjs": "^6.1.0",
}


Agregue las bibliotecas requeridas a angular.json



// angular.json

{
    ...,
    "architect": {
        "build": {
            ...,
            "scripts": [
                ...,
(+)            "node_modules/systemjs/dist/system.min.js",
(+)            "node_modules/systemjs/dist/extras/amd.min.js",
(+)            "node_modules/systemjs/dist/extras/named-exports.min.js",
(+)            "node_modules/systemjs/dist/extras/named-register.min.js",
(+)            "node_modules/import-map-overrides/dist/import-map-overrides.js"
             ]
        }
     }
},


Cree una carpeta de spa único en la raíz del proyecto . Agreguemos 2 archivos.



1. route-reuse-strategy.ts : nuestro archivo de enrutamiento de microservicios.

Si una aplicación secundaria está enrutando internamente, esa aplicación lo interpreta como un cambio de ruta.



    De forma predeterminada, esto destruirá el componente actual y lo reemplazará con una nueva instancia del mismo componente de spa-host.



Esta estrategia de reutilización de rutas analiza routeData.app para determinar si la nueva ruta debe tratarse como la misma ruta que la anterior, asegurando que no volvamos a montar la aplicación secundaria cuando la aplicación secundaria especificada se enruta internamente.



// route-reuse-strategy.ts

import { RouteReuseStrategy, ActivatedRouteSnapshot, DetachedRouteHandle } from '@angular/router';
import { Injectable } from '@angular/core';
@Injectable()
export class MicroFrontendRouteReuseStrategy extends RouteReuseStrategy {
    shouldDetach(): boolean {
        //   
        return false;
    }
    store(): void { }
    shouldAttach(): boolean {
        return false;
    }
    //   
    retrieve(): DetachedRouteHandle {
        return null;
    }
    shouldReuseRoute(future: ActivatedRouteSnapshot, curr: ActivatedRouteSnapshot): boolean {
        return future.routeConfig === curr.routeConfig || (future.data.app && (future.data.app === curr.data.app));
    }
}


2. Servicio single-spa.service.ts



El servicio almacenará el método para montar (montar) y desmontar (desmontar) aplicaciones de micro-frontend.



    mount es una función de ciclo de vida que se llamará siempre que no se monte una aplicación registrada, pero su función de actividad devuelve true. Cuando se llama, esta función debe mirar la URL para determinar la ruta activa y luego crear elementos DOM, eventos DOM, etc.



    unmount es una función de ciclo de vida que se llamará siempre que se monte una aplicación registrada, pero su función de actividad devuelve falso. Cuando se llama, esta función debería borrar todos los elementos DOM.



//single-spa.service.ts

import { Injectable } from '@angular/core';
import { mountRootParcel, Parcel, ParcelConfig } from 'single-spa';
import { Observable, from, of } from 'rxjs';
import { catchError, tap } from 'rxjs/operators';
@Injectable({
providedIn: 'root',
})
export class SingleSpaService {
    private loadedParcels: {
        [appName: string]: Parcel;
    } = {};
    mount(appName: string, domElement: HTMLElement): Observable<unknown> {
        return from(System.import<ParcelConfig>(appName)).pipe(
            tap((app: ParcelConfig) => {
                this.loadedParcels[appName] = mountRootParcel(app, {
                    domElement
                });
            })
        );
    }
    unmount(appName: string): Observable<unknown> {
        return from(this.loadedParcels[appName].unmount()).pipe(
            tap(( ) => delete this.loadedParcels[appName])
        );
    }
}


A continuación, creamos un contenedor de directorio / app / spa-host .



Este módulo implementará el registro y mapeo de nuestras aplicaciones de micro frontend a la raíz.



Agreguemos 3 archivos al módulo.



1. El módulo spa-host.module.ts en sí



//spa-host.module.ts

import { RouterModule, Routes } from '@angular/router';
import { CommonModule } from '@angular/common';
import { NgModule } from '@angular/core';
import { SpaUnmountGuard } from './spa-unmount.guard';
import { SpaHostComponent } from './spa-host.component';
const routes: Routes = [
    {
        path: '',
        canDeactivate: [SpaUnmountGuard],
        component: SpaHostComponent,
    },
];
@NgModule({
    declarations: [SpaHostComponent],
    imports: [CommonModule, RouterModule.forChild(routes)]
})
export class SpaHostModule {}


2. Componente spa-host.component.ts : coordina la instalación y el desmantelamiento de las aplicaciones de micro-frontend.



// spa-host.component.ts 

import { Component, OnInit, ViewChild, ElementRef, OnDestroy, ChangeDetectionStrategy } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
import { Observable } from 'rxjs';
import {SingleSpaService} from '../../single-spa/single-spa.service';
@Component({
selector: 'app-spa-host',
template: '<div #appContainer></div>',
changeDetection: ChangeDetectionStrategy.OnPush
})
export class SpaHostComponent implements OnInit {
    @ViewChild('appContainer', { static: true })
    appContainerRef: ElementRef;
    appName: string;
    constructor(private singleSpaService: SingleSpaService, private route: ActivatedRoute) { }
    ngOnInit() {
        //    
        this.appName = this.route.snapshot.data.app;
        this.mount().subscribe();
    }
     //       
    mount(): Observable<unknown> {
        return this.singleSpaService.mount(this.appName, this.appContainerRef.nativeElement);
    }
    // 
    unmount(): Observable<unknown> {
        return this.singleSpaService.unmount(this.appName);
    }
}


3. spa-unmount.guard.ts - comprueba si el nombre de la aplicación en la ruta es diferente, analiza el servicio anterior, si lo es también, simplemente ve a él. 



// spa-unmount.guard.ts

import { Injectable } from '@angular/core';
import { CanDeactivate, ActivatedRouteSnapshot, RouterStateSnapshot } from '@angular/router';
import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';
import { SpaHostComponent } from './spa-host.component';
@Injectable({ providedIn: 'root' })
export class SpaUnmountGuard implements CanDeactivate<SpaHostComponent> {
    canDeactivate(
        component: SpaHostComponent,
        currentRoute: ActivatedRouteSnapshot,
        currentState: RouterStateSnapshot,
        nextState: RouterStateSnapshot
    ): boolean | Observable<boolean> {
        const currentApp = component.appName;
        const nextApp = this.extractAppDataFromRouteTree(nextState.root);
        
        if (currentApp === nextApp) {
            return true;
        }
        return component.unmount().pipe(map(_ => true));
    }
    private extractAppDataFromRouteTree(routeFragment: ActivatedRouteSnapshot): string {
        if (routeFragment.data && routeFragment.data.app) {
            return routeFragment.data.app;
        }
        if (!routeFragment.children.length) {
            return null;
        }
        return routeFragment.children.map(r => this.extractAppDataFromRouteTree(r)).find(r => r !== null);    
    }
}


Registramos todo lo que agregamos al módulo de la aplicación.



// app.module.ts

providers: [
      ...,
      {
(+)     provide: RouteReuseStrategy,
(+)     useClass: MicroFrontendRouteReuseStrategy
      }
]


Cambiemos main.js.



// main.ts

import { enableProdMode, NgZone } from '@angular/core';
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
import { start as singleSpaStart } from 'single-spa';
import { getSingleSpaExtraProviders } from 'single-spa-angular';
import { AppModule } from './app/app.module';
import { PlatformLocation } from '@angular/common';
if (environment.production) {
    enableProdMode();
}
singleSpaStart();
//  

const appId = 'container-app';

//      ,     getSingleSpaExtraProviders. 
platformBrowserDynamic(getSingleSpaExtraProviders()).bootstrapModule(AppModule).then(module => {
    NgZone.isInAngularZone = () => {
    // @ts-ignore
        return window.Zone.current._properties[appId] === true;
    };
    const rootPlatformLocation = module.injector.get(PlatformLocation) as any;
    const rootZone = module.injector.get(NgZone);
    // tslint:disable-next-line:no-string-literal
    rootZone['_inner']._properties[appId] = true;
    rootPlatformLocation.setNgZone(rootZone);
})
.catch(err => {});


A continuación, creamos un archivo import-map.json en la carpeta compartida. El archivo es necesario para agregar mapas de importación.

Por el momento, lo tendremos vacío y se llenará a medida que se agreguen aplicaciones a la raíz.



<head>
<!doctype html>
<html lang="en">
<head>
       <meta charset="utf-8">
       <title>My first microfrontend root project</title>
       <base href="/">
       ...
(+)  <meta name="importmap-type" content="systemjs-importmap" />
    <script type="systemjs-importmap" src="/assets/import-map.json"></script>
</head>
<body>
    <app-root></app-root>
    <import-map-overrides-full></import-map-overrides-full>
    <noscript>Please enable JavaScript to continue using this application.</noscript>
</body>
</html>
    

4. Cree una microaplicación VUE (vue-app)



Ahora que hemos agregado la capacidad de convertirnos en una aplicación raíz a nuestro proyecto monolítico, es hora de crear nuestra primera microaplicación externa con spa único.



Primero, necesitamos instalar globalmente create-single-spa, una interfaz de línea de comandos que nos ayudará a crear nuevos proyectos de spa único con comandos simples.



Ir a la consola



npm install --global create-single-spa


Cree una aplicación vue simple usando un comando en la consola



create-single-spa


La interfaz de línea de comandos le pedirá que seleccione un directorio, nombre de proyecto, organización y tipo de aplicación para crear



imagen



? Directory for new project vue-app 
? Select type to generate single-spa application / parcel 
? Which framework do you want to use? vue 
? Which package manager do you want to use? npm 
? Organization name (use lowercase and dashes) somename 


Lanzamos nuestra microaplicación



npm i 
npm run serve --port 8000


Cuando ingresamos la ruta en el navegador localhost : 8080 / , en el caso de vue, veremos una pantalla en blanco. ¿Que pasó? 

Como no hay un archivo index.js en la microaplicación generada.  



Single-spa proporciona un área de juegos desde la que descargar la aplicación a través de Internet, así que usémosla primero.



Agregar a index.js 

single-spa-playground.org/playground/instant-test?name=@some-name/vue-app&url=8000
Al crear la aplicación raíz, agregamos un mapa por adelantado para cargar nuestro proyecto vue. 



{
"imports": {
    ... ,
    "vue": "https://unpkg.com/vue",     
    "vue-router": "https://cdn.jsdelivr.net/npm/vue-router@3.0.7/dist/vue-router.min.js",
    "@somename/vue-app": "//localhost:8080/js/app.js"
}
}


¡Listo! Ahora, desde nuestro proyecto de raíz angular, podemos cargar microaplicaciones escritas en vue.



5. Cree una micro-aplicación REACT (react-app)



Creamos una aplicación de reacción similarmente simple usando el comando en la consola



create-single-spa


Nombre de la organización: somename



Nombre del proyecto: react-app



? Directory for new project react-app 
? Select type to generate single-spa application / parcel 
? Which framework do you want to use? react 
? Which package manager do you want to use? npm 
? Organization name (use lowercase and dashes) somename 


Comprobemos si hemos agregado un mapa de importación en nuestra aplicación raíz.



{
"imports": {
    ... ,
       "react": "https://cdn.jsdelivr.net/npm/react@16.13.1/umd/react.development.js",
       "react-dom": "https://cdn.jsdelivr.net/npm/react-dom@16.13.1/umd/react-dom.development.js",
       "@somename/react-app": "//localhost:8081/somename-projname.js",
	}
}


¡Hecho! Ahora, en nuestra ruta react-app, cargamos el microproyecto react. 



6. Cree una micro-aplicación ANGULAR (angular-app)



Creamos una microaplicación Angular exactamente de la misma forma que las 2 anteriores



create-single-spa


Nombre de la organización: somename



Nombre del proyecto: angular-app



? Directory for new project angular-app 
? Select type to generate single-spa application / parcel 
? Which framework do you want to use? angular 
? Which package manager do you want to use? npm 
? Organization name (use lowercase and dashes) somename 


Comprobemos si hemos agregado un mapa de importación en nuestra aplicación raíz.



{
    "imports": {
        ... ,
       "@somename/angular-app": "//localhost:8082/main.js",
     }
}


Lanzamos, comprobamos, todo debería funcionar.



Este es mi primer post sobre Habré, estaré muy agradecido por tus comentarios.



All Articles