使用单spa创建微服务架构(迁移现有项目)

图片



这是关于该主题的第一篇文章,计划进行三篇文章:



  1. *从现有项目创建根应用程序,向其中添加3个微型应用程序(Vue,React,Angular)
  2. 微型应用程序之间的通信
  3. 使用git(部署,更新)


目录



  1. 一个共同的部分
  2. 为什么需要
  3. 从整体中创建一个根容器(请参见下面的定义)
  4. 创建一个VUE微型应用程序(vue-app)
  5.  创建一个微型应用程序REACT(反应应用程序)
  6.  创建一个微型应用程序ANGULAR(angular-app)


1.一般部分



本文的目的是增加使用现有的整体项目作为微服务体系结构的根容器的功能。



现有项目是在angular 9上完成的。



对于微服务架构,我们使用single-spa



您需要在根项目中添加3个项目,我们使用不同的技术:vue-app,angular-app,react-app(请参见第4、5、6页)。



在创建本文的同时,我试图将这种体系结构实现到我目前正在从事的生产项目中。因此,我将尝试描述开发过程中存在的所有错误及其解决方案。



根申请(以下称根))-我们应用程序的根目录(容器)。我们将所有微服务放入(注册)其中。如果您已经有一个项目并想要在其中实现此体系结构,那么现有项目将成为根应用程序,随着时间的推移,您将尝试从中根除应用程序的各个部分,创建单独的微服务并将其注册到此容器中。



这种创建根容器的方法将提供一个极好的机会,无需太多麻烦即可迁移到另一种技术。



例如,我们决定完全从有角度的发展到真正的目标,但是该项目是大胆的,目前它为企业带来了很多钱。



没有微服务架构,只有在那些相信独角兽并且我们都是全息图的绝望的人们面前,这才不会出现在我们的思想中。

为了在现实中切换到新技术,有必要重写整个项目,只有这样,我们才能从战斗中脱颖而出。



另一个选择是微服务架构。您可以从整体中创建一个根项目,在同一Vue上添加一个新项目,将漫游设置为根,完成。您可以投入战斗,从项目的根部逐渐切下小块,然后将其转移到vue微型项目中。这样只会在根容器中保留导入新项目所需的文件。



这可以在现在和现在进行,而不会丢失,流血,而且最重要的是,这是真实的。

由于将现有项目写入其中,因此我将使用angular作为根。



单页应用程序将被包装到的常规接口:



bootstrap(安装程序,总线) -在加载服务后调用,它将告诉您需要安装房屋的哪个元素,为它提供消息总线,微服务将订阅到该消息总线,并且能够侦听和发送请求以及



mount命令() -从本地安装应用程序



unmount() -拆卸应用程序



unload() -卸载应用程序



在代码中,我将再次在使用位置本地描述每种方法的操作。



2.为什么需要



让我们从严格的顺序开始。



有两种类型的体系结构:



  1. 整体式
  2. 微服务架构


图片



有了整体,一切对我们所有人来说都是非常简单和熟悉的。强大的凝聚力,巨大的代码块,共享的存储库,大量的方法。



最初,单片架构是尽可能方便和快捷的。创建任何集成文件,中间层,事件模型,数据总线等都没有问题和困难。



当您的项目增长时,就会出现问题,出现了许多用于不同目的的单独的,复杂的功能。所有这些功能开始在项目内与某种通用模型,状态,实用程序,接口,方法等联系在一起。



而且,随着时间的推移,项目中目录和文件的数量会变得巨大,存在着整个项目的查找和理解问题,“顶视图”丢失了,这使我们可以清楚地知道我们在做什么,在哪里以及需要谁。



除此之外,Eagleson定律还在起作用,它表示您六个月或更长时间没有看过的代码看起来就像是别人在写它。



最痛苦的是,一切都会成倍增长,因此,拐杖开始了,由于与以上相关的代码维护复杂性以及随着时间的推移,不负责任的条款浪潮的出现,必须添加拐杖。



结果,如果您有一个不断发展的实时项目,这将成为一个大问题,团队永恒的不满情绪,大量人员-对项目进行细微更改的时间,新员工的入门门槛低以及将项目付诸实践的大量时间。这一切都会导致混乱,好吧,我们喜欢秩序吗?



这种情况是否总是会发生?



当然不是!这完全取决于您的项目类型,以及团队开发过程中出现的问题。您的项目可能并不大,无法执行一项复杂的业务任务,这很正常,我认为这是正确的。



首先,我们需要注意项目的参数。 



我将尝试带您了解我们是否真的需要微服务架构的要点:



  • 2个或更多团队在从事该项目,前端开发人员的数量为10+;
  • 您的项目包含2个或多个业务模型,例如,您的网上商店中有大量商品,过滤器,通知和快递分发功能(2个相互影响的独立的,不小的业务模型)。所有这些都可以分开生活,而不是相互依赖。
  • UI功能集每天或每周增长,而不会影响系统的其余部分。


Microfront用于:



  • 前端的各个部分可以独立开发,测试和部署;
  • 无需重新组装即可添加,移除或更换前端的某些部件;
  •   .
  • , - «», - ( ) -.
  • ,
  • .


single-spa ?



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


以我的理解,微服务是一个独立的单页面应用程序,它将仅解决一个用户任务。该应用程序也不必解决团队的整个任务。 



SystemJS是一个开源JS库,通常用作浏览器的polyfill。



polyfill是一段JS代码,用于为不支持它的旧版浏览器提供现代功能。



SystemJS的功能之一是导入映射,它允许您通过网络导入模块并将其映射到变量名。



例如,您可以将导入映射用于通过CDN加载的React库:



BUT!



如果您是从头开始创建项目,即使考虑到已经确定了项目的所有参数,您也将决定拥有30人以上的大型Mega超级项目,请耐心等待!



我真的很喜欢微服务概念的臭名昭著的创始人马丁·福勒Martin Fowler)的想法



他建议将单块方法和微服务组合为一个(MonolithFirst)。其主要思想如下: 

即使您完全确信将来的应用程序将足够大以证明这种方法是正确的,也不应使用微服务来启动新项目。


我还将在这里描述使用这种架构的缺点:



  • 片段之间的相互作用无法通过标准试管方法(例如DI)实现。
  • 常见的依赖项呢?毕竟,如果不将它们从片段中取出,应用程序的大小将突飞猛进。
  • 在最终应用程序中,仍然应该有人负责路由。
  • 目前尚不清楚该如何处理不同的微服务可以位于不同的域上的事实
  • 如果片段之一不可用/无法呈现该怎么办。


3.创建一个根容器



因此,有足够的理论,是时候开始了。



转到控制台



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


在ts.config.app.json中,全局导入声明(类型)



// ts.config.app.json

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




将我们添加到root的所有微应用程序添加到app-routing.module.ts



// 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' }
        }
    ]
},


您还需要添加配置



// extra-webpack.config.json

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


让我们更改package.json文件,添加所有必要的工作或



// 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",
}


将所需的库添加到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"
             ]
        }
     }
},


在项目的根目录创建一个spa文件夹。让我们添加2个文件。



1. route-reuse-strategy.ts-我们的微服务路由文件。

如果子应用程序在内部进行路由,则该应用程序会将其解释为路由更改。



    默认情况下,这将销毁当前组件并将其替换为同一spa-host组件的新实例。



此路由重用策略查看routeData.app以确定是否将新路由与上一条路由视为相同的路由,以确保在指定的子应用程序内部路由时,我们不会重新挂载该子应用程序。



// 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.服务single-spa.service.ts



该服务将存储用于安装(卸载)和卸载(卸载)微前端应用程序的方法。



    mount是一个生命周期函数,每当未挂载已注册的应用程序,但其活动函数返回true时,就会调用该函数。调用此函数时,该函数应查看URL以确定活动路由,然后创建DOM元素,DOM事件等。



    unmount是一个生命周期函数,每当挂载已注册的应用程序时都会调用它,但其活动函数返回false。调用时,此函数应清除所有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])
        );
    }
}


接下来,我们创建一个目录container / app / spa-host



此模块将实现我们的微前端应用程序的注册和映射到root。



让我们向模块添加3个文件。



1. spa-host.module.ts模块本身



//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.组件spa-host.component.ts-协调微前端应用程序的安装和拆除



// 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-检查路由中的应用程序名称是否不同,解析先前的服务,如果相同,则转到它。 



// 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);    
    }
}


我们注册添加到app.module的所有内容



// app.module.ts

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


让我们更改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 => {});


接下来,我们在共享文件夹中创建一个文件import-map.json。添加导入映射需要该文件。

目前,我们将其清空,并在将应用程序添加到root时填满。



<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.创建一个VUE微型应用程序(vue-app)



现在,我们已经添加了成为根项目的功能,现在是时候使用单spa创建我们的第一个外部微应用程序了。



首先,我们需要全局安装create-single-spa,这是一个命令行界面,它将帮助我们使用简单的命令创建新的single-spa项目。



转到控制台



npm install --global create-single-spa


在控制台中使用命令创建简单的vue应用



create-single-spa


命令行界面将提示您选择要创建的目录,项目名称,组织和应用程序类型



图片



? 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 


我们启动微应用程序



npm i 
npm run serve --port 8000


当我们在浏览器localhost:8080 /中输入路径时,对于vue,我们将看到一个空白屏幕。发生了什么? 

由于生成的微型应用程序中没有index.js文件。  



Single SPA提供了一个游乐场,可以通过它在Internet上下载应用程序,因此让我们首先使用它。



添加到index.js 

single-spa-playground.org/playground/instant-test?name=@some-name/vue-app&url=8000
创建根应用程序时,我们预先添加了一个地图以加载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"
}
}


准备!现在,从我们的角度根项目中,我们可以加载用vue编写的微型应用程序。



5.创建一个微型应用程序REACT(react-app)



我们使用控制台中的命令创建一个类似的简单应用程序



create-single-spa


组织名称:somename



项目名称: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 


让我们检查是否在根应用程序中添加了导入映射



{
"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",
	}
}


做完了!现在,在我们的react-app路线上,我们加载了react微项目。 



6.创建一个微型应用程序ANGULAR(angular-app)



我们以与前2种完全相同的方式创建Angular微型应用程序



create-single-spa


组织名称:somename



项目名称: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 


让我们检查是否在根应用程序中添加了导入映射



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


我们启动,检查,一切都会正常。



这是我在哈布雷(Habré)上的第一篇文章,非常感谢您的评论。



All Articles