Anglarを利用したマルチチームモジュール化SPA開発フレーム


0、はじめに
一つの会社に複数の開発チームがある時、私達はこのような問題に出会うかもしれません。
1.技術オプションが乱雑で、みんなで各ゲームをします。
2.業務の重複度が高く、各種共通アプリ、登録抹消、権限管理は重複して実施する必要があります。
3.業務障壁、業務間の連絡が面倒になる
4.配置が複雑で、複数のドメイン名(またはIPアドレス)にアクセスして、ユーザーに大きな記憶困難をもたらします。
5.マルチシステムで、スタイルが統一しにくい
6.ちょっと待ってください
もちろん、解決方法はたくさんあります。以下はこちらの解決策を説明します。
1、考え方
アングラル
Anglar(注:非AnglarJS)は、人気の先端MVMフレームの一つであり、Type Scriptに合わせて、バックグラウンド管理システムを作るのに非常に適しています。私たちは今までのAnglarjsの開発フレームワークのセットのために、Anglarを選択し続けて実現し、可能な限りAnglarJS対応のモジュールを選択します。
SPA
SPAを選択しますか?それとも複数ページを選択しますか?余分なMvmは、複数ページが標準ではない。また、複数のページの開発において、私たちは必ずより多くの内容に注目します。
モジュール化
なぜモジュール化されますか?複数のチームが開発されている場合(またはプロジェクトが大きい場合)、各チームが開発したものはすべてモジュール(JSモジュールに限らず)でありたいです。このようにすれば、独立してモジュールを公開、更新、削除することができます。また、特定のモジュールに注目して開発効率とメンテナンス性を高めることができます。
プラットフォーム化
私たちは指定されたモジュールを実行できるようにプラットフォーム(Websiteサイト)が必要です。これにより単一の入口が実現でき、汎用論理、モジュール共有機構なども容易に実現できる。
AnglarJS対応モジュール
フレームをAnglarに切り替えたいと考えていますが、現在のモジュールとの互換性については避けられない問題があります。大体のオプションは以下の通りです。
1.AngularJS->Anglar公式アップデートガイドを参照して、一歩ずつモジュールをAnglarの実現に切り替えます。仕事量が多いので、開発チームで多くの調整が必要です)
2.iframeを組み込むと、一定の体験の差がありますが、開発チームにとっては、基本的にシームレスにアップグレードされています。何かを変える必要もありません。この案を選んだに違いない。
モジュール包装
私たちは単一のモジュールを資源パッケージとして包装して更新したいです。このようにしてモジュールが独立してリリースされ、適時に有効になります。
CSS衝突
大型SPAでは、CSS衝突が大きな問題です。我々は、技術的手段を通じて、現在使用されているモジュールに従って、CSSをロードし、アンロードすることができることを期待しています。
ページをまたいでデータを共有する
従来のiframe対応モジュールに関しては、ウィンドウをまたぐページ共有を考慮する必要があります。
共通モジュール
グループのモジュールが多いと、いくつかの公共のものが抽出されます。このプロセスでは、フレームワークは分かりません。だから、公共モジュールをサポートすることを考慮しなければなりません。モジュール間の依存関係もある)
3、実現する
上記のいくつかの思考に基づいて、まず基本的なプラットフォームのウェブサイトを実現する必要があります。これはあまり難しくないです。直接Anglarで実現すればいいです。このセットがあれば、私達の登録抹消、基本的なメニュー権限管理も実現します。
その上で、私達も公共サービス、公共部品を実現できます。
モジュール化はどうなりますか?包装はどうしますか?
このモジュールはアングラー自体のモジュールではありません。私たちは約束を通して、modules/下の各カタログは一つの業務モジュールです。一つの業務モジュールは一般的に静的資源、CSS及びJSを含みます。この考え方によって、私たちのパッケージポリシーは、modules/のすべてのディレクトリを巡回し、個々にパッケージ化し(webpack多entryパッケージ+CSS抽出)、またgulpを使って関連する静的リソースを処理します。
一般的に、webpackはすべての関連している依存関係を一緒に包装します。A、Bモジュールは@anglar/core識別に依存しています。そして、フレームの中にも@anglar関連のコンポーネントがすでに包装されています。この時、通常のパッケージ構成はあまりよくないです。どうすればいいですか?
AnglarもCDNバージョンを提供していることを考慮して、Anglarのコンポーネントをファイルに統合して、グローバル全体としてアクセスします。
このように、私達は包装する時、webpackのexternals機能を利用して、関連している依存を大域変数に置き換えることができます。

externals: [{
 'rxjs': 'Rx',
 '@angular/common': 'ng.common',
 '@angular/compiler': 'ng.compiler',
 '@angular/core': 'ng.core',
 '@angular/http': 'ng.http',
 '@angular/platform-browser': 'ng.platformBrowser',
 '@angular/platform-browser-dynamic': 'ng.platformBrowserDynamic',
 '@angular/router': 'ng.router',
 '@angular/forms': 'ng.forms',
 '@angular/animations': 'ng.animations'
}

このように処理してから、私たちが包装したファイルには、アングラーフレームコードがありません。
これは資源を導入する方法に対しても一定の要求があるので、直接に内部資源を導入することはできません。
モジュールを動的にロードする方法
包装が完了したら、この時はプラットフォームがどうやってこれらのモジュールをロードするかを考えます。
モジュールのロードはいつ決めますか?実は特定ルートを訪問する時です。だから私達のトップルートはPromise方法を使って実現します。

const loadModule = (moduleName) => {
 return () => {
  return ModuleLoaderService.load(moduleName);
 };
};

const dynamicRoutes = [];

modules.forEach(item => {
 dynamicRoutes.push({
  path: item.path,
  canActivate: [AuthGuard],
  canActivateChild: [AuthGuard],
  loadChildren: loadModule(item.module)
 });
});
const appRoutes: Routes = [{
 path: 'login', component: LoginComponent
}, {
 path: 'logout', component: LogoutComponent
}, {
 path: '', component: LayoutComponent, canActivate: [AuthGuard],
 children: [
  { path: '', component: HomeComponent },
  ...dynamicRoutes,
  { path: '**', component: NotFoundComponent },
 ]
}];
私たちはモジュールごとにumd形式で包装します。そしてこのモジュールを使う必要があるときは、ダイナミックビルドスクリプトを使ってスクリプトを実行します。

load(moduleName, isDepModule = false): Promise<any> {
 let module = window['xxx'][moduleName];
 if (module) {
  return Promise.resolve(module);
 }
 return new Promise((resolve, reject) => {
  let path = `${root}${moduleName}/app.js?rnd=${Math.random()}`;
  this._loadCss(moduleName);
  this.http.get(path)
   .toPromise()
   .then(res => {
    let code = res.text();
    this._DomEval(code);
    return window['xxx'][moduleName];
   })
   .then(mod => {
    window['xxx'][moduleName] = mod;
    let AppModule = mod.AppModule;
    // route change will call useModuleStyles function.
    // this.useModuleStyles(moduleName, isDepModule);
    resolve(AppModule);
   })
   .catch(err => {
    console.error('Load module failed: ', err);
    resolve(EmptyModule);
   });
 });
}

//   jQuery
_DomEval(code, doc?) {
 doc = doc || document;
 let script = doc.createElement('script');
 script.text = code;
 doc.head.appendChild(script).parentNode.removeChild(script);
}

CSSの動的ローディングは比較的簡単で、コードは以下の通りです。

_loadCss(moduleName: string): void {
 let cssPath = `${root}${moduleName}/app.css?rnd=${Math.random()}`;
 let link = document.createElement('link');
 link.setAttribute('rel', 'stylesheet');
 link.setAttribute('href', cssPath);
 link.setAttribute('class', `xxx-module-style ${moduleName}`);
 document.querySelector('head').appendChild(link);
}
モジュール切り替え時にアンインストールできるように、ルート切り替え時に使用する方法も提供されます。

useModuleStyles(moduleName: string): void {
 let xxxModuleStyles = [].slice.apply(document.querySelectorAll('.xxx-module-style'));
 let moduleDeps = this._getModuleAndDeps(moduleName);
 moduleDeps.push(moduleName);
 xxxModuleStyles.forEach(link => {
  let disabled = true;
  for (let i = moduleDeps.length - 1; i >= 0; i--) {
   if (link.className.indexOf(moduleDeps[i]) >= 0) {
    disabled = false;
    moduleDeps.splice(i, 1);
    break;
   }
  }
  link.disabled = disabled;
 });
}
共通モジュールの依存性
モジュール依存性を処理するために、AMD仕様を参考にして、キャリアとしてrequirejsを使用することができます。今は私の実現の中で、キャリアをカスタマイズしました。後期はAMD仕様に切り替えるはずです。AngularJSモジュールの互換性はどうなりますか?
AnglarJSのモジュールに対応するために、iframeを導入しました。iframeは先にAnglarJS宿主をロードして、この宿主の中でAnglarJSモジュールを実行します。通信を実現するためには、2つのプラットフォームプログラムの中から、postMessageに基づいて実現されるウインドウをまたぐ通信ライブラリを導入する必要があります。
AOTコンパイル
Anglar公式のAotコンパイルの流れに従ってください。
複数のTabページ
バックグラウンドシステムでは、複数のTabページがよく使われている。ただし、複数のTabページは、1ページで使用すると、一定の性能リスクがあり、これは実際の状況に基づいて使用されます。複数のTabページを実現するコアは、コンポーネントを動的にロードし、ロードするコンポーネントをどのように取得するかである。
複数のTabページは、実際にはTabsetコンポーネントであり、tab-intemの実現にはやや特別なものがあり、関連する動的なローディングソース:

@ViewChild('dynamicComponentContainer', { read: ViewContainerRef }) dynamicComponentContainer: ViewContainerRef;

constructor(
 private elementRef: ElementRef,
 private renderer: Renderer2,
 private tabset: TabsetComponent,
 private resolver: ComponentFactoryResolver,
 private parentContexts: ChildrenOutletContexts
) {
}

public destroy() {
 let el = this.elementRef.nativeElement as HTMLElement;
 // tslint:disable-next-line:no-unused-expression
 el.parentNode && (el.parentNode.removeChild(el));
}

private loadComponent(component: any) {
 let context = this.parentContexts.getContext(PRIMARY_OUTLET);
 let injector = ReflectiveInjector.fromResolvedProviders([], this.dynamicComponentContainer.injector);
 const resolver = context.resolver || this.resolver;
 let factory = resolver.resolveComponentFactory(component);
 //  let componentIns = factory.create(injector);
 //  this.dynamicComponentContainer.insert(componentIns.hostView);
 this.dynamicComponentContainer.createComponent(factory);
}

注意:コンポーネントのアンインストール方法を考慮します。destry()
現在レンダリングされているコンポーネントを得るために、私たちは道の由来を借りてキャプチャすることができます。

this.router.events.subscribe(evt => {
 if (evt instanceof NavigationEnd) {
  let pageComponent;
  let pageName;
  try {
   let nextRoute = this.route.children[0].children[0];
   pageName = this.location.path();
   pageComponent = nextRoute.component;
  } catch (e) {
   pageName = '$$notfound';
   pageComponent = NotFoundComponent;
  }
  let idx = this.pageList.length + 1;
  if (!this.pageList.find(x => x.name === pageName)) {
   this.pageList.push({
    header: `  ${idx}`,
    comp: pageComponent,
    name: pageName,
    closable: true
   });
  }
  setTimeout(() => {
   this.selectedPage = pageName;
  });
 }
});

3、まとめ
以上は大体の実現の構想と部分の関連している細い点です。その他の細かいところは実際の状況によって情状を酌量して処理する必要があります。
この考え方はAnglarフレームに限らず、Vue、Reactを使っても同様の効果が得られます。同時に、このセットは中小企業のバックグラウンドプラットフォームにも適しています。
詳細を知る必要があれば、gx-modular-plotformを参照してください。
本論文のgithubアドレス
以上が本文の全部です。皆さんの勉強に役に立つように、私たちを応援してください。