マイクロフロントエンドに関しては、多くの利点と課題が同様に存在します。独立したデプロイメントと、依存関係、ロジック、コンポーネントなどの共有をどのように解決したかをご覧ください。
簡単に言うと、マイクロサービスがバックエンドのためのものであるように、マイクロフロントエンドはフロントエンドのためのものです。これは、モノリシックなウェブアプリケーションを、分散した小さな個々のアプリケーションに変換することで実現されます。場合によっては、これらの個々のアプリケーションは独立したアプリケーションとして動作し、独立して開発、デプロイすることができます。さらに、他のアプリケーションと組み合わせれば、1つのアプリケーションとして動作させることができます。
Harnessでは、モノリシックなUIアプリケーションを分割するためにマイクロフロントエンドアーキテクチャーを採用し、以下の目標を達成したいと考えました。
- パフォーマンス。アプリケーションの全ての部分が全てのユーザーによって使用されるわけではありません。複雑なアプリの特定の部分をオンデマンドでロードする機能は、アプリケーションの初期ロード時間に有益です。
- 独立したデプロイメント。Harnessは複雑なアプリケーションです。そのため、さまざまなモジュールで構成されています。マイクロフロントエンドアーキテクチャーを採用すれば、現在のように全てのモジュールを一度にデプロイするのではなく、個々のモジュールを別の子アプリとして分割し、独立してデプロイすることができます。
- 開発速度。モジュールを独立して実行できるため、大幅 に加速させることができます。これにより、1つのモジュール内の機能を修正・実装するために、アプリケーション全体を実行するオーバーヘッドを取り除くことができます。
- 開発者の認知的負荷。1つのモジュールに取り組んでいても、それを縮小してアプリケーション全体を理解することができます。
マイクロフロントエンドの課題
マイクロフロントエンド実装のために市販されているさまざまなソリューションを探し始めたとき、私たちは次のような点をカバーすることを求めました。
- 依存関係の共有。依存関係の共有を簡単に扱えるソリューションが欲しかったのです。アプリケーションに依存するリソース、例えば共通ライブラリー、React、Lodash、React Routerなどです。これらは、子アプリと親アプリの両方で使用されるものがいくつかあります。選択されたアーキテクチャーは、子が動的にロードされたときに、共通リソースを再ロードしないようにする必要があります。
- データの共有。親と子の間でデータを共有する必要がある場合、望ましいソリューションは最も効率的な方法で双方向のデータ転送を処理できることです。
- リソースの共有。子アプリと親アプリで、いくつかの共通のUIコンポーネントを共有する必要があります。これは、全てのモジュールで一貫して見える必要があり、同じものが既に親アプリ内で利用可能である場合は、子で再ロードされることはありません。
- 独立したデプロイメント。親アプリを再デプロイすることなく、子モジュールを独立してデプロイし、親アプリがロードされるたびに最新の子モジュールをロードできるソリューションが必要でした。
- 独立した開発。開発者が子アプリをローカルで独立に実行できるようなソリューションが望ましいと考えました。
マイクロフロントエンドソリューションの種類
上記の課題は、大きく2つに分類されます。
- 子アプリが親アプリにどのようにプラグインされるのか。
- 子アプリにどのようにデータ/リソースを共有させるか。
そこで、この2つの問題を解決するために、次のような解決策を考え出しました。
子アプリをプラグインする
- Webpackモジュールフェデレーション。Webpackのバンドルツールを使って、親が子アプリを動的にロードする場所から、特定のパスで子アプリを実行するようにします。
- モジュールをアプリとしてインポートする。子アプリは、サードパーティーライブラリーと同じ方法で読み込まれます。これ以前は、子アプリもライブラリーとして公開されていました。
- iframe。iframeを使って別の場所で動作する子アプリを読み込んでいます。
子アプリへのデータ/リソース共有
- マネージラッパー。上記のいずれかの方法で子アプリをロードします。しかし、親にロードされるとき、親から子へ、またはその逆をラッパーがラップしてデータ転送します。
Webpackモジュールフェデレーション
これは最も一般的な方法で、Webpackという名前のバンドルツールが、子アプリを特定のパスやポートでサービスとして公開するのを支援しま す。子アプリを動的にロードしたいときに、親は子パスをヒットするようにあらかじめ設定されています。
以下の図では、Harnessアプリケーションがシェルアプリまたは親アプリ、CIやCVなどはHarnessアプリケーション内のさまざまなモジュールです。
長所
- モジュールのバンドル/ロードと依存関係の共有を箱から出して処理します。
- 完全なアプリは実行時にのみ1つにまとめられます。
短所
- データ共有、リソース共有が解決しません。
- ビルドツールとしてWebpackにロックインされています。
モジュールをアプリとしてインポートする
この方法では、子アプリをnpmパッケージとしてバンドルし、そのパッケージを公開します。その後、子パッケージを他のnpmパッケージとしてダウンロードし、親アプリで他のライブラリーと同様に使い始めます。
長所
- 静的インポートでは、子プロセスが型を公開している限り、型安全性が確保されます。
- 一定の制約のもと、モジュールを独立して開発することができます。
- バンドルが1つのため、マイクロUIの読み込みにネットワーク遅延がありません。
短所
- レイジーローディングは実装に余計な手間がかかります。
- 親アプリ(NGUI)の構築は子アプリもバンドルするため高コストです。
- 子アプリは親アプリからコードをインポートできないため、リソースの共有が難しいです。
- 独立した子アプリのデプロイができません。
Managed Wrapper
この方法では、親アプリコードのdivなどのHTML要素の中で子アプリを実行します。子コードを取得する方法は、上で説明した2つのアプローチのいずれでもかまいません。このアプローチの背後にある考え方は、親アプリの内部で子アプリをラップできるラッパーを書くことであり、それはまた、子へデータを渡す責任も負います。このラッパーは、親アプリのデータ形式と子アプリのデータ形式を理解できるように記述されます。
長所
- 子アプリは、単一のレンダリングまたはビルドフレームワークに制限されることはありません。
- 子アプリは、依存性注入によって親からリソースやコンポーネントを使用できます。
- 親は、子が利用できる共有データを制御します。
短所
- これは、データとリソースの共有のみを解決するものです。それでも、バンドルと依存関係の共有のために、先の2つのアプローチのうちの1つと組み合わせる必要があります。
Iframe
多くの組織がマイクロフロントエンドを実装するために採用する、最も一般的なアプローチの1つです。このアプローチでは、子アプリは、独立したアプリケーションとして、指定された事前定義されたパスで実行されます。親がこの子アプリをロードする必要がある場合、親は単に、事前に定義されたパスを使用してiframeで子アプリをロードします。この方法の主な欠点は、セキュリティー上の問題が多いことでした。さらに、上記のアプローチと比較すると、リソースの共有さえもシームレスではないことが分かりました。
長所
- 素朴なソリューションとしては最も実装しやすいです。
- 子アプリを完全に分離します。
短所
- セキュリティー上の懸念があります(CSPの設定を正しくすれば解決する可能性があります)。
- 依存関係の共有は非常に困難です。
- データの共有は非常にコストがかかります(シリアライゼーション/デシリアライゼーションが必要)。
マイクロフロントエンドの取り組み
子モジュールをWebpackのモジュールフェデレーションを使って、別のサービスとして、別のパス/ポートで動かしています。
親アプリで子アプリを動的にロードする場合、その子アプリをラッパーの内部で実行します。これにより、親アプリと子アプリの間のデータ/リソースの共有が行われます。
親アプリは、子アプリが親のデータ形式を理解するために、子アプリと共有したいリソース/データのデータ型を含むパッケージを公開します。
子アプリで動作するラッパーが親データを子で理解できるデータに変換する機能を持っていれば、子アプリは好きなフレームワークで自由に動作でき、必ずしも親フレームワーク(Reactなど)を実行する必要はないのです。
長所
- データとリソースの共有が容易です。
- レイジーローディングができます。
- 親バンドルが小さいです。
- 双方向のデータ共有が可能です。
- 子アプリのデプロイメントと開発は独立しています。
- 単一のサードパーティーライブラリーに依存しません。
- リソース共有を親が制御できます。
- NGUIへの変更は最小限です。
- Harnessのカスタムであるため、将来的に自由に実験できます。
- サードパーティーライブラリーに依存しないので、オープンソースに貢献します。
短所
マイクロフロント実装ステップ
- 親アプリからパッケージとして型/インターフェイスを公開します。
- 親は子がどのようにレンダリングされるべきかのインターフェイスを公開します。
- 子モジュールを別のアプリケーション/サービスとして実行します。
- 子モジュールをレイジーロードします。
- 実行時に親から子アプリに共通のコンポーネント/コンテキストデータを渡します。
以下は、私たちのアプローチの手順を示しています。
データ型の共有
データフロー
結論
私たちのマイクロフロントエンドソリューションを使うことで、子アプリを親アプリの中のラッパーに動的にロードすることができます。これは、子アプリと親アプリの間でデータを送受信するプロキシーのように動作します。子アプリの共通コンポーネントは、再ロードすることなく親アプリから再利 用することができました。また、このアプローチにより、親アプリに依存することなく、必要なときに子アプリを個別にデプロイすることができました。
また、開発者がローカルで作業する際に、親アプリを実行することなく、子アプリを独立して実行することもできました。親アプリの種類を事前に公開していたため、子アプリを扱う開発者は、データの構造に関する知識を得ることができました。これは親アプリで使用され、子アプリに渡されるため、開発者が子アプリで作業する際に非常に役に立ちました。
どうやら、専門的な記事がお好きなようですね。さらに素晴らしいコンテンツをご用意しています。テストのためのビルダーファクトリーパターンやReactでの文字列の外部化についてもお読みください。
この記事は、Swaraj Cheguri、Abhinav Rastogiの協力で執筆されました。
この記事はHarness社のウェブサイトで公開 されているものをDigital Stacksが日本語に訳したものです。無断複製を禁じます。原文はこちらです。