2020 年上半期に、Webpack は非常にエキサイティングな機能である Module Federation (モジュール フェデレーションと訳されます) をリリースしました。この機能は、リリースされるやいなや業界で広く注目を集め、業界ではゲーム チェンジャーとさえ呼ばれています。フロントエンド構築の様子。実際、このテクノロジーは複数のアプリケーション モジュールの再利用の問題を実際に解決し、そのソリューションは以前のソリューションよりもエレガントで柔軟です。しかし、別の観点から見ると、モジュール フェデレーションは一般的なソリューションであり、特定のビルド ツールに限定されないため、この機能を Vite に実装することもでき、コミュニティはすでにソリューションを成熟させています。
1. モジュール共有の苦痛
インターネット製品の場合、通常、さまざまな細分化されたアプリケーションがあり、たとえば、Tencent ドキュメントは Word、Excel、ppt などのカテゴリに分類でき、Douyin PC サイトはショート ビデオ サイト、ライブ ブロードキャスト サイトなどのサブサイトに分類できます。 、検索サイト、および各サブステーションは互いに独立しており、異なる開発チームによって独立して開発および保守される可能性があります。問題がないように見えますが、実際には、モジュールの共有に関するいくつかの問題が発生することがよくあります。これは、さまざまなアプリケーションで常に問題が発生することを意味しており、パブリック コンポーネント、パブリック ユーティリティ関数、パブリック サードパーティの依存関係などの一部の共有コードには問題が発生します。これらの共有コードについて、単純なコピー アンド ペースト以外に再利用するより良い方法はあるでしょうか?
一般的なコードの再利用方法をいくつか示します。
1.1 npmパッケージをリリース
npm パッケージの公開は、モジュールを再利用する一般的な方法であり、いくつかの共通コードを npm パッケージにパッケージ化し、この npm パッケージを他のプロジェクトで参照できます。具体的なリリース更新プロセスは次のとおりです。
- パブリック ライブラリ lib1 が変更され、npm に公開されました。
- すべてのアプリケーションは新しい依存関係をインストールし、共同デバッグを実行します。
npm パッケージをカプセル化するとモジュールの再利用の問題は解決できますが、新たな問題が発生します。
- 開発効率の問題。すべての変更をリリースする必要があり、関連するすべてのアプリケーションが新しい依存関係をインストールする必要がありますが、このプロセスはより複雑です。
- プロジェクトのビルドの問題。パブリック ライブラリの導入後、パブリック ライブラリのコードをプロジェクトの最終製品にパッケージ化する必要があるため、製品サイズが大きくなり、ビルド速度が比較的遅くなります。
したがって、この解決策は最終的な解決策として使用することはできず、問題を一時的に解決するための無力な手段にすぎません。
1.2 Git サブモジュール
git サブモジュールを通じて、コードをパブリック Git リポジトリにカプセル化して、さまざまなアプリケーションで再利用できますが、次の手順も実行する必要があります。
- パブリック ライブラリ lib1 への変更を Git リモート ウェアハウスに送信します。
- すべてのアプリケーションは git submodule コマンドを通じてサブウェアハウス コードを更新し、共同デバッグを実行します。
全体的なプロセスは npm パッケージの送信とほぼ同じですが、npm パッケージのソリューションにはまださまざまな問題があることがわかります。
1.3 外部化 + CDN 導入に依存する
いわゆる依存関係の外部化 (外部) とは、一部のサードパーティの依存関係を構築に参加させる必要がなく、特定のパブリック コードを使用することを意味します。この考え方によれば、ビルド エンジンで特定の依存関係に対して external を宣言し、依存する CDN アドレスを HTML に追加できます。次に例を示します。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/src/favicon.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Vite App</title>
</head>
<body>
<div id="root"></div>
<!-- 从 CDN 上引入第三方依赖的代码 -->
<script src="https://cdn.jsdelivr.net/npm/[email protected]/index.min.js"><script>
<script src="https://cdn.jsdelivr.net/npm/[email protected]/index.min.js"><script>
</body>
</html>
上の例に示すように、CDN を使用して、通常は UMD 形式の製品を使用して React と React-dom をインポートすることができます。これにより、異なるプロジェクトが window.React を通じて同じ依存コードを使用できるようになり、モジュールの再利用効果が実現します。ただし、このアプローチには次のような制限もあります。
- 互換性の問題。すべての依存関係に UMD 形式の製品があるわけではないため、このソリューションはすべてのサードパーティの npm パッケージをカバーすることはできません。
- 依存関係の順序の問題。通常、間接的な依存関係の問題を考慮する必要があります。たとえば、antd コンポーネント ライブラリの場合、react と moment にも依存するため、react と moment も外部必要であり、これらのパッケージは HTML で参照され、参照の順序は次のとおりです。 antdの後ろにmomentを置くとコードが動かなくなる可能性があると言われています。サードパーティのパッケージの背後にある間接的な依存関係の数は通常膨大であり、それらを 1 つずつ処理していくと、開発者にとって悪夢となるでしょう。
- 製品の量の問題。依存パッケージが外部宣言された後、アプリケーションがその CDN アドレスを参照するとき、依存コードを完全に参照することになります。この場合、ツリー シェーキングによって不要なコードを削除する方法がなく、アプリケーションのパフォーマンスが低下します。 。
1.4 モノレポ
新しいプロジェクト管理方法として、Monorepo はモジュールの再利用の問題もうまく解決できます。Monorepo アーキテクチャでは、複数のプロジェクトを同じ Git ウェアハウスに配置でき、相互依存する各サブプロジェクトはソフト チェーンを通じてデバッグされます。コードの再利用は非常に便利です。依存するコードの変更がある場合は、この依存関係を使用します。プロジェクトですぐに感じました。
Monorepo はアプリケーション間でのモジュールの再利用の問題に対する非常に優れた解決策であることは認めざるを得ませんが、同時に使用にはいくつかの制限もあります。
- すべてのアプリケーション コードは同じリポジトリに配置する必要があります。古いプロジェクトで各アプリケーションが Git リポジトリを使用している場合、Monorepo を使用した後のプロジェクト構造の調整は比較的大きくなり、変換コストが比較的高くなります。
- Monorepo自体にも当然の限界があり、例えばプロジェクト数が増えると依存関係のインストールに時間がかかり、プロジェクト全体の構築時間が長くなるなど、開発効率の問題も解決する必要があります。これらの制限によって引き起こされる問題。また、この作業は通常、専門の人材による解決が必要となるため、人材への投資やインフラの保証が十分でない場合には、Monorepo は適切な選択肢とはならない可能性があります。
- プロジェクトのビルドの問題。npm パッケージを送信するソリューションと同様に、すべてのパブリック コードをプロジェクトの構築プロセスに組み込む必要がありますが、それでも製品のサイズは大きすぎます。
第二に、モジュールフェデレーションの中心的な概念
次に、モジュール フェデレーション、つまりモジュール フェデレーション ソリューションを正式に導入し、それがモジュールの再利用の問題をどのように解決するかを見てみましょう。モジュール フェデレーションには主に、ローカル モジュールとリモート モジュールの 2 種類のモジュールがあります。
ローカル モジュールは現在のビルド プロセスの一部である共通モジュールですが、リモート モジュールは現在のビルド プロセスの一部ではなく、ローカル モジュールの実行中にインポートされます。次の図に示すように、モジュールは一部の依存コードを共有できます。
モジュールフェデレーションでは、各モジュールがローカルモジュールになって他のリモートモジュールをインポートすることも、リモートモジュールになって他のモジュールによってインポートされることもできることを強調する価値があります。次の例に示すように:
上記はモジュール フェデレーションの主な設計原則ですが、次にこの設計の利点を分析しましょう。
- 任意の粒度でモジュール共有を実現します。ここで言うモジュールの粒度は、サードパーティの npm 依存関係、ビジネス コンポーネント、ツール機能、さらにはフロントエンド アプリケーション全体を含めて、大小さまざまです。フロントエンド アプリケーション全体で製品を共有できます。これは、各アプリケーションが独立して開発、テスト、展開されることを意味し、これもマイクロ フロント エンドの実現です。
- ビルド製品のボリュームを最適化します。リモート モジュールは、ローカル モジュールの構築に参加せずにローカル モジュールのランタイムから取得できるため、構築プロセスを高速化して構築アーティファクトを減らすことができます。
- 実行時にオンデマンドでロードされます。リモート モジュールのインポートの粒度は非常に小さくすることができます。app1 モジュールの add 関数のみを使用したい場合は、この関数を app1 のビルド構成でエクスポートし、それを import( としてローカル モジュールにインポートするだけです。 'app1/add') これで、オンデマンドでモジュールをロードできるようになります。
- サードパーティの依存関係は共有されます。モジュールフェデレーションにおける依存関係の共有メカニズムにより、モジュール間の依存関係コードの共通化を容易に実現でき、これまでの外部+CDN導入スキームのさまざまな問題を回避できます。
上記の分析から、モジュールフェデレーションはこれまでのモジュール共有の問題をほぼ完全に解決し、アプリケーションレベルの共有を実現することもでき、それによってマイクロフロントエンドの効果を実現できることがわかります。次に、具体的な例を使用して、Vite のモジュール フェデレーション機能を使用してコードの再利用を解決する方法を学びましょう。
3. モジュールフェデレーションの適用
コミュニティは、比較的成熟した Vite モジュール フェデレーション ソリューション、vite-plugin-federation を提供しています。これは、Vite (または Rollup) に基づいた完全なモジュール フェデレーション機能を実装します。次に、それをベースにしてモジュールフェデレーションアプリケーションを実装します。まず、ホストとリモートの 2 つの Vue スキャフォールディング プロジェクトを初期化し、vite-plugin-federation プラグインをそれぞれインストールします。コマンドは次のとおりです。
npm install @originjs/vite-plugin-federation -D
次に、次の構成コードを構成ファイル vite.config.ts に追加します。
// 远程模块配置
// remote/vite.config.ts
import { defineConfig } from "vite";
import vue from "@vitejs/plugin-vue";
import federation from "@originjs/vite-plugin-federation";
// https://vitejs.dev/config/
export default defineConfig({
plugins: [
vue(),
// 模块联邦配置
federation({
name: "remote_app",
filename: "remoteEntry.js",
// 导出模块声明
exposes: {
"./Button": "./src/components/Button.js",
"./App": "./src/App.vue",
"./utils": "./src/utils.ts",
},
// 共享依赖声明
shared: ["vue"],
}),
],
// 打包配置
build: {
target: "esnext",
},
});
// 本地模块配置
// host/vite.config.ts
import { defineConfig } from "vite";
import vue from "@vitejs/plugin-vue";
import federation from "@originjs/vite-plugin-federation";
export default defineConfig({
plugins: [
vue(),
federation({
// 远程模块声明
remotes: {
remote_app: "http://localhost:3001/assets/remoteEntry.js",
},
// 共享依赖声明
shared: ["vue"],
}),
],
build: {
target: "esnext",
},
});
上記の構成では、リモート モジュールのモジュール エクスポートとローカル モジュールへのリモート モジュールの登録が完了しました。リモート モジュールの具体的な実装については、Githubウェアハウスのコードを参照してください。次に、リモート モジュールの使用方法に焦点を当てましょう。
まず、リモート モジュールをパッケージ化し、リモート パスの下の実行コマンドに依存する必要があります。
// 打包产物
pnpm run build
// 模拟部署效果,一般会在生产环境将产物上传到 CDN
npx vite preview --port=3001 --strictPort
次に、ホスト プロジェクトでリモート モジュールを使用します。サンプル コードは次のとおりです。
<script setup lang="ts">
import HelloWorld from "./components/HelloWorld.vue";
import { defineAsyncComponent } from "vue";
// 导入远程模块
// 1. 组件
import RemoteApp from "remote_app/App";
// 2. 工具函数
import { add } from "remote_app/utils";
// 3. 异步组件
const AysncRemoteButton = defineAsyncComponent(
() => import("remote_app/Button")
);
const data: number = add(1, 2);
</script>
<template>
<div>
<img alt="Vue logo" src="./assets/logo.png" />
<HelloWorld />
<RemoteApp />
<AysncRemoteButton />
<p>应用 2 工具函数计算结果: 1 + 2 = {
{ data }}</p>
</div>
</template>
<style>
#app {
font-family: Avenir, Helvetica, Arial, sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
text-align: center;
color: #2c3e50;
margin-top: 60px;
}
</style>
次に、npm run dev でプロジェクトを開始すると、次の結果が表示されます。
アプリケーション 2 のコンポーネントとユーティリティ関数ロジックは、アプリケーション 1 ですでに有効になっています。つまり、ローカル モジュールへのリモート モジュールのランタイム インポートが完了しています。全体的な使用プロセスを整理してみましょう。
- リモート モジュールはエクスポーズを通じてエクスポートされたモジュールを登録し、ローカル モジュールはリモートを通じてリモート モジュール アドレスを登録します。
- リモート モジュールが構築され、クラウドにデプロイされます。
- リモート モジュールは、「リモート モジュール名/xxx」をインポートすることでローカルに導入され、ランタイム ロードが実現されます。
4. モジュールフェデレーションの実装原理
上記の例から、モジュール フェデレーションの使用は比較的簡単で、既存のプロジェクトの変換コストはそれほど大きくないことがわかります。では、このような強力で使いやすい機能はどのようにして Vite で実現されるのでしょうか? 次に、MF の背後にある実装原理を詳しく調べ、その背後で vite-plugin-federation プラグインが何を行っているかを分析しましょう。
全体として、モジュール フェデレーションを実現するには 3 つの主要な要素があります。
- ホスト モジュール: リモート モジュールを使用するために使用されるローカル モジュールです。
- リモート モジュール: これはリモート モジュールであり、いくつかのモジュールを生成し、ローカル モジュールで使用できるランタイム コンテナを公開するために使用されます。
- 共有依存関係: 共有依存関係は、ローカル モジュールとリモート モジュール間でサードパーティの依存関係を共有するために使用されます。
まず、ローカル モジュールがリモート モジュールをどのように消費するかを見てみましょう。以前、ローカル モジュールに次のようなインポート ステートメントを書きました。
import RemoteApp from "remote_app/App";
Vite がこのコードをどのようにコンパイルするかを見てみましょう。
// 为了方便阅读,以下部分方法的函数名进行了简化
// 远程模块表
const remotesMap = {
'remote_app':{url:'http://localhost:3001/assets/remoteEntry.js',format:'esm',from:'vite'},
'shared':{url:'vue',format:'esm',from:'vite'}
};
async function ensure() {
const remote = remoteMap[remoteId];
// 做一些初始化逻辑,暂时忽略
// 返回的是运行时容器
}
async function getRemote(remoteName, componentName) {
return ensure(remoteName)
// 从运行时容器里面获取远程模块
.then(remote => remote.get(componentName))
.then(factory => factory());
}
// import 语句被编译成了这样
// tip: es2020 产物语法已经支持顶层 await
const __remote_appApp = await getRemote("remote_app" , "./App");
コンパイルされる import ステートメントに加えて、remoteMap といくつかのユーティリティ関数がコードに追加されていることがわかりますが、その目的は非常に単純で、リモート ランタイム コンテナーにアクセスして、対応する名前のモジュールをプルすることです。ランタイム コンテナーは実際には、リモート モジュール パッケージング製品 (remoteEntry.js) のエクスポートされたオブジェクトを参照します。そのロジックを見てみましょう。
// remoteEntry.js
const moduleMap = {
"./Button": () => {
return import('./__federation_expose_Button.js').then(module => () => module)
},
"./App": () => {
dynamicLoadingCss('./__federation_expose_App.css');
return import('./__federation_expose_App.js').then(module => () => module);
},
'./utils': () => {
return import('./__federation_expose_Utils.js').then(module => () => module);
}
};
// 加载 css
const dynamicLoadingCss = (cssFilePath) => {
const metaUrl = import.meta.url;
if (typeof metaUrl == 'undefined') {
console.warn('The remote style takes effect only when the build.target option in the vite.config.ts file is higher than that of "es2020".');
return
}
const curUrl = metaUrl.substring(0, metaUrl.lastIndexOf('remoteEntry.js'));
const element = document.head.appendChild(document.createElement('link'));
element.href = curUrl + cssFilePath;
element.rel = 'stylesheet';
};
// 关键方法,暴露模块
const get =(module) => {
return moduleMap[module]();
};
const init = () => {
// 初始化逻辑,用于共享模块,暂时省略
}
export { dynamicLoadingCss, get, init }
ランタイム コンテナのコードから、いくつかの重要な情報を引き出すことができます。
- moduleMap はエクスポートされたモジュールの情報を記録するために使用されます。exposes パラメーターで宣言されたすべてのモジュールは別のファイルにパッケージ化され、動的インポートを通じてインポートされます。
- コンテナーは非常に重要な get メソッドをエクスポートするため、ローカル モジュールはこのメソッドを呼び出すことでリモート モジュールにアクセスできます。
ここまでは、以下の図に示すように、リモート モジュールのランタイム コンテナとローカル モジュール間の対話プロセスを整理しました。
次に、共有依存関係の実装の分析に進みます。前のサンプル プロジェクトを例に挙げると、ローカル モジュールがshared: ['vue'] パラメーターを設定した後、リモート モジュールのコードを実行するときに、vue を導入する状況に遭遇すると、ローカル vue の使用を優先します。モジュール内のリモート vue の代わりに。
コンテナーの初期化のロジックに焦点を当て、ローカル モジュールがコンパイルされた後、ensure 関数のロジックに戻りましょう。
// host
// 下面是共享依赖表。每个共享依赖都会单独打包
const shareScope = {
'vue':{'3.2.31':{get:()=>get('./__federation_shared_vue.js'), loaded:1}}
};
async function ensure(remoteId) {
const remote = remotesMap[remoteId];
if (remote.inited) {
return new Promise(resolve => {
if (!remote.inited) {
remote.lib = window[remoteId];
remote.lib.init(shareScope);
remote.inited = true;
}
resolve(remote.lib);
});
}
}
ensure 関数の主なロジックは、共有依存関係情報をリモート モジュールのランタイム コンテナに渡し、コンテナを初期化することであることがわかります。次に、コンテナ初期化の論理初期化に入ります。
const init =(shareScope) => {
globalThis.__federation_shared__= globalThis.__federation_shared__|| {};
// 下面的逻辑大家不用深究,作用很简单,就是将本地模块的`共享模块表`绑定到远程模块的全局 window 对象上
Object.entries(shareScope).forEach(([key, value]) => {
const versionKey = Object.keys(value)[0];
const versionValue = Object.values(value)[0];
const scope = versionValue.scope || 'default';
globalThis.__federation_shared__[scope] = globalThis.__federation_shared__[scope] || {};
const shared= globalThis.__federation_shared__[scope];
(shared[key] = shared[key]||{})[versionKey] = versionValue;
});
};
ローカル モジュールの共有依存関係テーブルにリモート モジュールでアクセスできる場合、ローカル モジュール (vue など) の依存関係をリモート モジュールでも使用できます。次に、リモート モジュールの import { h } from 'vue' のインポート コードが以下のようにどのように変換されるかを見てみましょう。
// __federation_expose_Button.js
import {importShared} from './__federation_fn_import.js'
const { h } = await importShared('vue')
サードパーティ依存モジュールの処理ロジックが importShared 関数に集中していることは難しくありません。調べてみましょう。
// __federation_fn_import.js
const moduleMap= {
'vue': {
get:()=>()=>__federation_import('./__federation_shared_vue.js'),
import:true
}
};
// 第三方模块缓存
const moduleCache = Object.create(null);
async function importShared(name,shareScope = 'default') {
return moduleCache[name] ?
new Promise((r) => r(moduleCache[name])) :
getProviderSharedModule(name, shareScope);
}
async function getProviderSharedModule(name, shareScope) {
// 从 window 对象中寻找第三方包的包名,如果发现有挂载,则获取本地模块的依赖
if (xxx) {
return await getHostDep();
} else {
return getConsumerSharedModule(name);
}
}
async function getConsumerSharedModule(name , shareScope) {
if (moduleMap[name]?.import) {
const module = (await moduleMap[name].get())();
moduleCache[name] = module;
return module;
} else {
console.error(`consumer config import=false,so cant use callback shared module`);
}
}
共有依存関係情報は、リモート モジュールの実行中にコンテナが初期化されるときにマウントされているため、リモート モジュールは現在の依存関係が共有依存関係であるかどうかを簡単に認識できます。共有依存関係である場合は、ローカル モジュールの依存コードを使用します。それ以外の場合は、リモート モジュールを使用します。独自の依存製品コードの概略図は次のとおりです。
V. まとめ
まず、モジュールの再利用の問題に対する歴史的な解決策、主に npm パッケージの公開、Git サブモジュール、外部化 + CDN インポートへの依存、および Monorepo アーキテクチャを紹介し、それぞれの利点と制限も分析し、モジュールを紹介しました。フェデレーション (MF) の概念を分析し、モジュール共有の問題をほぼ完全に解決できる理由を分析しました。その主な理由には、モジュール共有の任意の粒度の実現、ビルド製品のサイズの削減、実行時のオンデマンドのロード、および 3 番目の共有が含まれます。パーティの依存関係の側面。
次に、具体的なプロジェクト例を使用して、Vite のモジュールフェデレーション機能の使用方法、つまり vite-plugin-federation プラグインを通じて MF の構築を完了する方法を説明します。最後に、MF の基礎となる実装原理についても詳しく説明し、ローカル モジュール、リモート モジュール、共有依存関係の 3 つの観点から MF の実装メカニズムとコア コンパイル ロジックを分析しました。