この記事では、マイクロフロントエンドの概念とシナリオの一般的な概要を説明し、いくつかの主流のマイクロフロントエンド実装ライブラリとその使用法を紹介し、これらのライブラリのいくつかの原理と実践的な知識について説明します。
I. マイクロフロントエンド
プロジェクトの反復において、ビジネスが成長し発展するにつれて、プロジェクトは通常、より多くの機能モジュールを持つようになります。すべてのコードモジュールが1つのリポジトリにあり、1つのチームがそれらに責任を持つことになるかもしれません。しかし、機能モジュールが増えるにつれ、1つのチームが責任を負えなくなり、異なるモジュールを保守するために複数のチームが必要になるかもしれません。対応するコードは複数のリポジトリに分割され、各モジュールは独立して開発、デプロイ、更新されます。通常、プロジェクトは複数のモジュールに分割されますが、全体の統一性とユーザーエクスペリエンスを維持するために、各モジュールは統一されたポータルの下に吊るされます。
上記のシナリオは、バックエンドのマイクロサービス・アーキテクチャに似た典型的なマイクロフロントエンド・シナリオであり、ウェブアプリケーションを単一のモノリシック・アプリケーションから、複数の小さなフロントエンド・アプリケーションに集約した1つのアプリケーションに変えます。
通常、上記のような要件を実現するためには、iframeを使うのが簡単です。iframeを使ってサブモジュールのページをエントリーフレームに表示し、サブモジュールを切り替えると、iframeも対応するサブモジュールのページのURLに切り替わります。
iframeの実装は比較的簡単ですが、通常はいくつかの問題があります:
- 例えば、サブプロジェクトでポップアップウィンドウマスクを表示する場合、マスクはページ全体ではなくiframe領域のみをカバーし、コンテンツは真の中央には表示されません。
- ページの閲覧履歴は自動的に記録されず、iframeはページを更新すると自動的にホームページに戻ります。
- グローバルコンテキストは完全に分離され、変数は共有されず、サブプロジェクトとテーマフレームワーク、サブプロジェクト間の通信など、postMessageでしかできないページ間通信は面倒です。
- 処理速度が遅くなり、サブアプリケーションに入るたびにコンテキスト全体を再構築します。
上記の問題の中には、解決できるものもあれば、解決できないもの、解決するのが難しいものもあります。全体として、iframe はより速い解決策ですが、最良の解決策ではありません。あらゆる種類のパッチを強要すれば、また複雑さが出てくるでしょうし、結局のところ、それだけの価値はないかもしれません。
シングル-spa
マイクロフロントエンドのiframe実装のデメリットについてお話ししましたが、その主な理由は、これらのアプリケーションがまだ独自の独立したページ内にあり、いくつかの自然な制限につながることです。シングルスパマイクロフロントエンドソリューションは、MPAとSPAの利点を組み合わせて、複数のアプリケーションを単一のページ内に統合し、技術スタックにとらわれません。
上の図は、シングルスパを使用したマイクロフロントエンド実装の全体的な流れを示しています:
サブプロジェクトの初期化リソースをロードするために使用されます。サブプロジェクトのエントリ js を umd フォーマットにビルドし、モジュールローダを使ってリモートでロードします。
異なるサブアプリケーションを切り替えるときにモジュールローダーを使ってリモートからロードできるように、それぞれのサブアプリケーションのエントリリソースの url 情報を記録するために使われます。エントリーリソースのハッシュは通常、サブアプリケーションが更新されるたびに変更されるので、フレームワークがサブアプリケーションの最新のリソースを時間内にロードできるように、サーバー側は設定テーブルを定期的に更新する必要があります。
single-spa自体はサブアプリケーションのリソースリストをサポートしていないため、各サブアプリケーションはすべての初期化リソースを1つのエントリjsにパッケージすることしかできません。サブアプリケーションの初期化リソースに複数のファイルがある場合、上記のような処理を追加する必要があります。
フレームワークの入り口
<!DOCTYPE html>
<html>
<head>
<!-- systemjsにモジュールを登録する-->
<script type="systemjs-importmap">
{
"imports": {
"app1": "http://://.js",
"app2": "http://://.js",
"single-spa": "https://..///-/..//-..js",
"vue": "https://..//@..//.js",
"vue-router": "https://..//-@..//-..js",
"vuex": "https://..////../..js"
}
}
</script>
</head>
<body>
<div></div>
<!-- systemjsを読み込む-->
<script src="https://..////../..js"></>
<script src="https://..////..//..js"></>
<script src="https://..////..//-.js"></>
<script src="https://..////..//-..js"></>
<script src="https://..////..//-..js"></>
<script>
(function () {
// 公開されているjsライブラリを読み込む
Promise.all([System.import('single-spa'), System.import('vue'), System.import('vue-router'), System.import('vuex')]).then(function (modules) {
var singleSpa = modules[0];
var Vue = modules[1];
var VueRouter = modules[2];
var Vuex = modules[3];
Vue.use(VueRouter)
Vue.use(Vuex)
// single-spaサブアプリケーションを登録する
singleSpa.registerApplication(
'app1',
() => System.import('app1'),
location => location.pathname.startsWith('/app1')
)
singleSpa.registerApplication(
'app2',
() => System.import('app2'),
location => location.pathname.startsWith('/app2')
)
//
singleSpa.start();
})
})()
</script>
</body>
</html>
エントリーローダー側が合意された仕様に従ってロードされたリソースを解析し、シングルスパライフサイクルフックに従ってリソースのマウントを処理する限り、サブアプリケーションリソースプロファイルは完全にカスタマイズ可能です。
それぞれのサブアプリケーションがこれらのライブラリファイルをインクルードする必要がないように、いくつかのパブリックリソースライブラリライブラリをエントリに抽出することもできます。これらのライブラリはサブアプリケーションでビルドするときに外部化する必要があります。たとえば、次のようにwebpackでビルドするときです:
externals: ['vue', 'vue-router', 'vuex']
、サブアプリケーションの入り口
import './set-public-path'
import Vue from 'vue'
import App from './App.vue'
import router from './router'
import singleSpaVue from 'single-spa-vue'
Vue.config.productionTip = false
if (process.env.NODE_ENV === 'development') {
// 開発環境から直接レンダリングする
new Vue({
router,
render: h => h(App)
}).$mount('#app')
}
const vueLifecycles = singleSpaVue({
Vue,
appOptions: {
render: (h) => h(App),
router
}
})
export const bootstrap = vueLifecycles.bootstrap
export const mount = vueLifecycles.mount
export const unmount = vueLifecycles.unmount
set-public-path.js
注意深い学生は、サブアプリケーションのコードがset-public-path.jsを実行していることに気づくでしょう。見てみましょう:
import { setPublicPath } from 'systemjs-webpack-interop'
setPublicPath('app1', 2)
setPublicPathのコードは以下の通りです:
export function setPublicPath(systemjsModuleName, rootDirectoryLevel) {
if (!rootDirectoryLevel) {
rootDirectoryLevel = 1;
}
if (
typeof systemjsModuleName !== "string" ||
systemjsModuleName.trim().length === 0
) {
throw Error(
"systemjs-webpack-interop: setPublicPath(systemjsModuleName) must be called with a non-empty string 'systemjsModuleName'"
);
}
if (
typeof rootDirectoryLevel !== "number" ||
rootDirectoryLevel <= 0 ||
!Number.isInteger(rootDirectoryLevel)
) {
throw Error(
"systemjs-webpack-interop: setPublicPath(systemjsModuleName, rootDirectoryLevel) must be called with a positive integer 'rootDirectoryLevel'"
);
}
let moduleUrl;
try {
moduleUrl = window.System.resolve(systemjsModuleName);
if (!moduleUrl) {
throw Error()
}
} catch (err) {
throw Error(
"systemjs-webpack-interop: There is no such module '" +
systemjsModuleName +
"' in the SystemJS registry. Did you misspell the name of your module?"
);
}
__webpack_public_path__ = resolveDirectory(moduleUrl, rootDirectoryLevel);
}
function resolveDirectory(urlString, rootDirectoryLevel) {
const url = new URL(urlString);
const pathname = new URL(urlString).pathname;
let numDirsProcessed = 0,
index = pathname.length;
while (numDirsProcessed !== rootDirectoryLevel && index >= 0) {
const char = pathname[--index];
if (char === "/") {
numDirsProcessed++;
}
}
if (numDirsProcessed !== rootDirectoryLevel) {
throw Error(
"systemjs-webpack-interop: rootDirectoryLevel (" +
rootDirectoryLevel +
") is greater than the number of directories (" +
numDirsProcessed +
") in the URL path " +
fullUrl
);
}
url.pathname = url.pathname.slice(0, index + 1);
return url.href;
}
III.シングルスパの欠点
上述したように、サブアプリケーションの初期化リソースに複数のファイルがある場合、サブアプリケーションのリソースのリストを管理し、自分で追加処理を行う必要がありますが、これも面倒なことが多いです;
複数のサブアプリケーションが1つのページに統合されている場合、cssとjsの両方が衝突する可能性が非常に高くなります。各サブアイテムに一意な名前のプレフィックスを使用するなどの仕様を設定することは可能ですが、そのような人為的な規約は信頼性が低いことがよくあります。cssの場合は、ビルド時にプレフィックスを自動的に追加するツールもあり、競合を回避する信頼性が高くなります。jsの場合は、jsのサブアプリケーションがそれぞれのサンドボックスで実行されるように、人工的なサンドボックスを作成するのがより信頼性の高い方法かもしれませんが、これは実現が複雑です。
四、乾坤
実際、シングルスパベースのオープンソースライブラリqiankunは、上記の問題を解決するのに役立っており、次のような特徴があります:
サブアプリケーションのエントリを解析するとき、解析されるのはjsファイルではなく、直接解析されるサブアプリケーションのhtmlファイルです。サブアプリケーションが更新されても、そのエントリのhtmlファイルのurlは常に変更されず、すべての初期リソースのurlを含むので、サブアプリケーションのリソースリストを自分で管理する必要がなくなります。
サブアプリケーションがマウントされると、いくつかの特別な処理が自動的に行われ、サブアプリケーションのすべてのリソースdomがサブアプリケーションルートノードdomの下に集約されるようにします。サブアプリケーションがアンインストールされると、対応するdom全体が削除され、スタイルの衝突も回避されます。
jsサンドボックスを提供し、サブアプリケーションがマウントされると、グローバルウィンドウオブジェクトをプロキシし、グローバルイベントリスナーをハイジャックするなどして、各サブアプリケーションが独自のサンドボックスで実行されるようにします。
複数のスパアプリケーションを含むデモ
サブアプリケーションのdom構造は以下の通りです。
もちろん、ますます大規模で複雑化するフロントエンドのシナリオにおいて、マイクロフロントエンドソリューションは銀の弾丸ではありませんが、探求し実践する価値のある方向性であることは確かです。




