シングルスパを使用したマイクロサービスアーキテクチャの作成(既存のプロジェクトの移行)

画像



これはこのトピックに関する最初の記事であり、合計3つが計画されています。



  1. *既存のプロジェクトからルートアプリケーションを作成し、それに3つのマイクロアプリケーションを追加します(vue、react、angular)
  2. マイクロアプリケーション間の通信
  3. gitの操作(デプロイ、更新)


目次



  1. 共通部分
  2. なぜそれが必要なのですか
  3. モノリスからルートコンテナ(以下の定義を参照)を作成します
  4. VUEマイクロアプリを作成する(vue-app)
  5.  マイクロアプリケーションREACT(react-app)を作成する
  6.  マイクロアプリケーションANGULAR(angular-app)を作成する


1.一般的な部分



この記事の目的は、既存のモノリシックプロジェクトをマイクロサービスアーキテクチャのルートコンテナとして使用する機能を追加することです。



既存のプロジェクトは



angular9で作成されています。マイクロサービスアーキテクチャでは、シングルスパライブラリを使用します



ルートプロジェクトに3つのプロジェクトを追加する必要があります。さまざまなテクノロジーを使用しています:vue-app、angular-app、react-appp。4、5、6を参照)。



この記事の作成と並行して、現在取り組んでいる本番プロジェクトにこのアーキテクチャを実装しようとしています。したがって、開発プロセスで発生するすべてのエラーとその解決策について説明しようと思います。



ルートアプリケーション(以下、ルート)-アプリケーションのルート(コンテナ)。すべてのマイクロサービスをその中に入れます(登録します)。すでにプロジェクトがあり、その中にこのアーキテクチャを実装したい場合は、既存のプロジェクトがルートアプリケーションになります。そこから、時間の経過とともにアプリケーションの一部をかじり、個別のマイクロサービスを作成して、このコンテナに登録します。



ルートコンテナを作成するこのアプローチは、それほど苦労せずに別のテクノロジーに移行する絶好の機会を提供します。



たとえば、角度からビューに完全に移行することにしましたが、プロジェクトは大胆であり、現時点ではビジネスに多額の資金をもたらしています。



マイクロサービスアーキテクチャがなければ、これは頭に浮かぶことはなかったでしょう。ユニコーンを信じ、私たち全員がホログラムであると信じている絶望的な人々だけに。

実際に新しい技術に切り替えるためには、プロジェクト全体を書き直す必要があります。そうして初めて、戦闘での出現から高くなることができます。



もう1つのオプションは、マイクロサービスアーキテクチャです。モノリスからルートプロジェクトを作成し、同じビューに新しいプロジェクトを追加し、ルートへのローミングを設定すれば完了です。戦いに注ぎ込み、プロジェクトのルートから徐々に小さな断片を切り取り、それらをvueマイクロプロジェクトに転送することができます。これにより、新しいプロジェクトをインポートするために必要なファイルのみがルートコンテナに残ります。



これは今ここで、損失や血を流さずに行うことができ、そして最も重要なことは本物です。

既存のプロジェクトが書かれているので、ルートとしてangularを使用します。



シングルページアプリケーションがラップされる一般的なインターフェイス:



bootstrap(mounter、bus) -サービスのロード後に呼び出され、マウントする必要がある家の要素を通知し、マイクロサービスがサブスクライブするメッセージバスを提供し、リクエストと



マウントコマンドをリッスンして送信できるようにします() -ホームからアプリケーションを



マウントアンマウント() -アプリケーションを解体



アンロード() -アプリケーションをアンロード



コードでは、使用場所でローカルに各メソッドの操作をもう一度説明します。



2.なぜそれが必要なのですか



この時点から厳密に順番に始めましょう。



アーキテクチャには2つのタイプがあります。



  1. モノリス
  2. マイクロサービスアーキテクチャ


画像



モノリスを使用すると、すべてが非常にシンプルで、私たち全員にできるだけ馴染みがあります。強力な結束、コードの巨大なブロック、共有リポジトリ、一連のメソッド。



当初、モノリシックアーキテクチャは可能な限り便利で高速です。統合ファイル、中間層、イベントモデル、データバスなどの作成に問題や問題はありません。



プロジェクトが大きくなると問題が発生し、さまざまな目的のために個別の複雑な機能が多数表示されます。このすべての機能は、プロジェクト内で、ある種の一般的なモデル、状態、ユーティリティ、インターフェイス、メソッドなどに結び付けられ始めます。



また、プロジェクト内のディレクトリやファイルの数は時間の経過とともに膨大になり、プロジェクト全体の検索と理解に問題があり、「上から見た」ことが失われ、私たちが何をしているのか、どこにあるのか、誰がそれを必要としているのかが明確になります。



これらすべてに加えて、イーグルソンの法則が機能しています。これは、6か月以上見ていなかったコードが、他の誰かが書いたように見えることを示しています。



最も苦痛なことは、すべてが指数関数的に成長し、その結果、クラッチが始まります。これは、上記に関連してコードを維持することの複雑さと、時間の経過とともに発生する無責任な用語の波のために追加する必要があります。



その結果、絶えず進化しているライブプロジェクトがある場合、これは大きな問題になります。チームの永遠の不満、プロジェクトに小さな変更を加えるための膨大な数の時間、新入社員の低いエントリしきい値、およびプロジェクトを戦闘に展開するための多くの時間です。これはすべて無秩序につながります、まあ、私たちは秩序が好きですか?



これは常にモノリスで発生しますか?



もちろん違います!それはすべて、プロジェクトのタイプ、チーム開発中に発生する問題によって異なります。あなたのプロジェクトはそれほど大きくないかもしれません、いくつかの複雑なビジネスタスクを実行するために、これは正常であり、私はそれが正しいと信じています。



まず、プロジェクトのパラメータに注意を払う必要があります。 



マイクロサービスアーキテクチャが本当に必要かどうかを理解できるポイントを取り上げてみます。



  • 2つ以上のチームがプロジェクトに取り組んでおり、フロントエンド開発者の数は10人以上です。
  • プロジェクトは2つ以上のビジネスモデルで構成されています。たとえば、膨大な数の商品、フィルター、通知、および宅配便の配送配布の機能を備えたオンラインストアがあります(互いに干渉する2つの別個の小規模なビジネスモデルではありません)。これはすべて別々に生きることができ、お互いに依存することはありません。
  • UI機能のセットは、システムの他の部分に影響を与えることなく、毎日または毎週拡張されます。


マイクロフロントは次の目的で使用されます。



  • フロントエンドの個別のパーツを個別に開発、テスト、および展開できます。
  • フロントエンドの部品は、再組み立てせずに追加、削除、または交換できます。
  •   .
  • , - «», - ( ) -.
  • ,
  • .


single-spa ?



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


私の理解では、マイクロサービスは、1つのユーザータスクのみを解決する独立した単一ページのアプリケーションです。このアプリケーションは、チームのタスク全体を解決する必要もありません。 



SystemJSは、ブラウザのポリフィルとして一般的に使用されるオープンソースのJSライブラリです。



ポリフィルは、それをサポートしていない古いブラウザに最新の機能を提供するために使用されるJSコードの一部です。



SystemJSの機能の1つはインポートマップです。これを使用すると、ネットワーク経由でモジュールをインポートして、変数名にマップできます。



たとえば、CDNを介してロードされるReactライブラリのインポートマップを使用できます







プロジェクトを最初から作成する場合は、プロジェクトのすべてのパラメーターを決定したことを考慮しても、30人以上のチームで構成される巨大なメガスーパープロジェクトを作成することにしました。お待ちください。



私は、マイクロサービスのアイデアの悪名高い創設者であるマーティン・ファウラーのアイデアが本当に好きです。



彼は、モノリシックアプローチとマイクロサービスを1つに統合することを提案しました(MonolithFirst)。その主なアイデアは次のとおりです。 

将来のアプリケーションがこのアプローチを正当化するのに十分な大きさになると完全に確信している場合でも、マイクロサービスで新しいプロジェクトを開始しないでください。


ここでは、このようなアーキテクチャを使用することの欠点についても説明します。



  • フラグメント間の相互作用は、標準のチューブ法(DIなど)では実現できません。
  • 一般的な依存関係はどうですか?結局のところ、アプリケーションのサイズは、フラグメントから取り出されない場合、飛躍的に大きくなります。
  • 最終的なアプリケーションでのルーティングは、引き続き誰かが担当する必要があります。
  • さまざまなマイクロサービスがさまざまなドメインに配置される可能性があるという事実をどうするかは明確ではありません
  • フラグメントの1つが使用できない/レンダリングできない場合の対処方法。


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


プロジェクトのルートにシングルスパフォルダを作成します。 2つのファイルを追加しましょう。



1. route-reuse-strategy.tsは、マイクロサービスのルーティングファイルです。

子アプリケーションが内部でルーティングしている場合、そのアプリケーションはこれをルート変更として解釈します。



    デフォルトでは、これにより現在のコンポーネントが破棄され、同じスパホストコンポーネントの新しいインスタンスに置き換えられます。



このルート再利用戦略は、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])
        );
    }
}


次に、ディレクトリコンテナ/ app / spa-hostを作成します



このモジュールは、マイクロフロントエンドアプリケーションの登録とルートへのマッピングを実装します。



モジュールに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ファイルを作成します。このファイルは、インポートマップを追加するために必要です。

現時点では、アプリケーションがルートに追加されると、空になり、いっぱいになります。



<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)を作成します



モノリシックプロジェクトにルートアプリケーションになる機能を追加したので、シングルスパを使用して最初の外部マイクロアプリケーションを作成します。



まず、create-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ファイルがないため。  



シングルスパは、インターネット経由でアプリケーションをダウンロードするための遊び場を提供するので、最初にそれを使用しましょう。



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


起動、確認、すべてが機能するはずです。



これはハブレへの私の最初の投稿です、私はあなたのコメントに非常に感謝します。



All Articles