複数の個別のビルドが単一のアプリケーションを形成する必要があります。これらの個別のビルドはコンテナのように機能し、ビルド間でコードを公開および消費して、単一の統合されたアプリケーションを作成できます。
これはマイクロフロントエンドとしてよく知られていますが、それだけには限定されません。
ローカルモジュールとリモートモジュールを区別します。ローカルモジュールは、現在のビルドの一部である通常のモジュールです。リモートモジュールは、現在のビルドの一部ではなく、実行時にリモートコンテナからロードされるモジュールです。
リモートモジュールのロードは、非同期操作と見なされます。リモートモジュールを使用する場合、これらの非同期操作は、リモートモジュールとエントリポイントの間にある次のチャンクロード操作に配置されます。チャンクロード操作なしでリモートモジュールを使用することはできません。
チャンクロード操作は通常import()
呼び出しですが、require.ensure
やrequire([...])
のような古い構成もサポートされています。
コンテナは、特定のモジュールへの非同期アクセスを公開するコンテナエントリを介して作成されます。公開されたアクセスは2つのステップに分かれています。
ステップ1は、チャンクロード中に行われます。ステップ2は、他の(ローカルおよびリモート)モジュールとインターリーブして、モジュール評価中に行われます。この方法では、ローカルからリモートにモジュールを変換するか、その逆の場合でも、評価順序は影響を受けません。
コンテナをネストすることは可能です。コンテナは他のコンテナのモジュールを使用できます。コンテナ間の循環依存関係も可能です。
各ビルドはコンテナとして機能し、他のビルドもコンテナとして消費します。この方法では、各ビルドは、コンテナからロードすることで、公開されている他のモジュールにアクセスできます。
共有モジュールは、オーバーライド可能であり、ネストされたコンテナへのオーバーライドとして提供されるモジュールです。これらは通常、各ビルドで同じモジュール(たとえば、同じライブラリ)を指します。
packageName
オプションを使用すると、requiredVersion
を検索するパッケージ名を設定できます。これはデフォルトでモジュールリクエストに対して自動的に推論されます。自動推論を無効にする場合は、requiredVersion
をfalse
に設定します。
このプラグインは、指定された公開モジュールを使用して追加のコンテナエントリを作成します。
このプラグインは、コンテナへの特定の参照を外部として追加し、これらのコンテナからリモートモジュールをインポートできるようにします。また、これらのコンテナのoverride
APIを呼び出して、それらにオーバーライドを提供します。ローカルオーバーライド(ビルドがコンテナでもある場合は、__webpack_override__
またはoverride
APIを介して)と、指定されたオーバーライドが、参照されるすべてのコンテナに提供されます。
ModuleFederationPlugin
は、ContainerPlugin
とContainerReferencePlugin
を組み合わせたものです。
config.context
を基準にして解決されます。requiredVersion
を使用しません。requiredVersion
を抽出します。/
が付いたモジュールリクエストは、このプレフィックスが付いたすべてのモジュールリクエストと一致します。シングルページアプリケーションの各ページは、個別のビルドのコンテナビルドから公開されます。アプリケーションシェルも、すべてのページをリモートモジュールとして参照する個別のビルドです。このようにして、各ページを個別にデプロイできます。ルートが更新されたり、新しいルートが追加されたりすると、アプリケーションシェルがデプロイされます。アプリケーションシェルは、ページビルドでの重複を避けるために、一般的に使用されるライブラリを共有モジュールとして定義します。
多くのアプリケーションは、各コンポーネントが公開されたコンテナとしてビルドできる共通のコンポーネントライブラリを共有しています。各アプリケーションは、コンポーネントライブラリコンテナからコンポーネントを消費します。コンポーネントライブラリへの変更は、すべてのアプリケーションを再デプロイする必要なく、個別にデプロイできます。アプリケーションは、コンポーネントライブラリの最新バージョンを自動的に使用します。
コンテナインターフェースは、get
メソッドと init
メソッドをサポートしています。init
は async
互換のメソッドで、引数として共有スコープオブジェクトを受け取って呼び出されます。このオブジェクトは、リモートコンテナ内で共有スコープとして使用され、ホストから提供されたモジュールで満たされます。これは、リモートコンテナをランタイム時に動的にホストコンテナに接続するために活用できます。
init.js
(async () => {
// Initializes the shared scope. Fills it with known provided modules from this build and all remotes
await __webpack_init_sharing__('default');
const container = window.someContainer; // or get the container somewhere else
// Initialize the container, it may provide shared modules
await container.init(__webpack_share_scopes__.default);
const module = await container.get('./module');
})();
コンテナは共有モジュールを提供しようとしますが、共有モジュールがすでに使用されている場合、警告が表示され、提供された共有モジュールは無視されます。コンテナはフォールバックとしてそれを使用する可能性があります。
このようにして、共有モジュールの異なるバージョンを提供するA/Bテストを動的にロードできます。
例
init.js
function loadComponent(scope, module) {
return async () => {
// Initializes the shared scope. Fills it with known provided modules from this build and all remotes
await __webpack_init_sharing__('default');
const container = window[scope]; // or get the container somewhere else
// Initialize the container, it may provide shared modules
await container.init(__webpack_share_scopes__.default);
const factory = await window[scope].get(module);
const Module = factory();
return Module;
};
}
loadComponent('abtests', 'test123');
一般的に、リモートは次の例のようにURLを使用して設定されます。
module.exports = {
plugins: [
new ModuleFederationPlugin({
name: 'host',
remotes: {
app1: 'app1@http://localhost:3001/remoteEntry.js',
},
}),
],
};
しかし、このリモートにPromiseを渡すこともでき、これはランタイム時に解決されます。上記の get/init
インターフェースに適合する任意のモジュールでこのPromiseを解決する必要があります。たとえば、クエリパラメータを介して、使用する必要があるフェデレーションモジュールのバージョンを渡したい場合は、次のようにすることができます。
module.exports = {
plugins: [
new ModuleFederationPlugin({
name: 'host',
remotes: {
app1: `promise new Promise(resolve => {
const urlParams = new URLSearchParams(window.location.search)
const version = urlParams.get('app1VersionParam')
// This part depends on how you plan on hosting and versioning your federated modules
const remoteUrlWithVersion = 'http://localhost:3001/' + version + '/remoteEntry.js'
const script = document.createElement('script')
script.src = remoteUrlWithVersion
script.onload = () => {
// the injected script has loaded and is available on window
// we can now resolve this Promise
const proxy = {
get: (request) => window.app1.get(request),
init: (arg) => {
try {
return window.app1.init(arg)
} catch(e) {
console.log('remote container already initialized')
}
}
}
resolve(proxy)
}
// inject this script with the src set to the versioned remoteEntry.js
document.head.appendChild(script);
})
`,
},
// ...
}),
],
};
このAPIを使用する場合、必ずget/init APIを含むオブジェクトを解決する必要があることに注意してください。
リモートモジュールからメソッドを公開することで、ホストがランタイム時にリモートモジュールのpublicPathを設定できるようにすることができます。
このアプローチは、独立してデプロイされた子アプリケーションをホストドメインのサブパスにマウントする場合に特に役立ちます。
シナリオ
https://my-host.com/app/*
でホストされているホストアプリと、https://foo-app.com
でホストされている子アプリがあるとします。子アプリもホストドメインにマウントされているため、https://foo-app.com
はhttps://my-host.com/app/foo-app
を介してアクセスできることが期待され、https://my-host.com/app/foo-app/*
のリクエストはプロキシを介してhttps://foo-app.com/*
にリダイレクトされます。
例
webpack.config.js (リモート)
module.exports = {
entry: {
remote: './public-path',
},
plugins: [
new ModuleFederationPlugin({
name: 'remote', // this name needs to match with the entry name
exposes: ['./public-path'],
// ...
}),
],
};
public-path.js (リモート)
export function set(value) {
__webpack_public_path__ = value;
}
src/index.js (ホスト)
const publicPath = await import('remote/public-path');
publicPath.set('/your-public-path');
//bootstrap app e.g. import('./bootstrap.js')
document.currentScript.src
からスクリプトタグからpublicPathを推測し、ランタイム時に__webpack_public_path__
モジュール変数で設定できます。
例
webpack.config.js (リモート)
module.exports = {
entry: {
remote: './setup-public-path',
},
plugins: [
new ModuleFederationPlugin({
name: 'remote', // this name needs to match with the entry name
// ...
}),
],
};
setup-public-path.js (リモート)
// derive the publicPath with your own logic and set it with the __webpack_public_path__ API
__webpack_public_path__ = document.currentScript.src + '/../';
Uncaught Error: Shared module is not available for eager consumption
アプリケーションは、全方向ホストとして動作しているアプリケーションをすぐに実行しています。選択肢があります。
モジュールフェデレーションの高度なAPI内で依存関係をeagerとして設定できます。これにより、モジュールは非同期チャンクには配置されませんが、同期的に提供されます。これにより、初期チャンクでこれらの共有モジュールを使用できます。ただし、提供されたモジュールとフォールバックモジュールはすべて常にダウンロードされるため、注意が必要です。アプリケーションの1箇所(シェルなど)でのみ提供することをお勧めします。
非同期境界を使用することを強くお勧めします。これにより、大きなチャンクの初期化コードが分割され、追加のラウンドトリップを回避し、全体的なパフォーマンスが向上します。
たとえば、エントリが次のようになっているとします。
index.js
import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';
ReactDOM.render(<App />, document.getElementById('root'));
bootstrap.js
ファイルを作成し、エントリの内容をそれに移動し、そのブートストラップをエントリにインポートします。
index.js
+ import('./bootstrap');
- import React from 'react';
- import ReactDOM from 'react-dom';
- import App from './App';
- ReactDOM.render(<App />, document.getElementById('root'));
bootstrap.js
+ import React from 'react';
+ import ReactDOM from 'react-dom';
+ import App from './App';
+ ReactDOM.render(<App />, document.getElementById('root'));
この方法は機能しますが、制限や欠点がある可能性があります。
ModuleFederationPlugin
を介して依存関係にeager: true
を設定する
webpack.config.js
// ...
new ModuleFederationPlugin({
shared: {
...deps,
react: {
eager: true,
},
},
});
Uncaught Error: Module "./Button" does not exist in container.
おそらく "./Button"
とは表示されませんが、エラーメッセージは同様になります。この問題は、webpack beta.16からwebpack beta.17にアップグレードする場合によく見られます。
ModuleFederationPlugin内で、exposesを次のように変更します。
new ModuleFederationPlugin({
exposes: {
- 'Button': './src/Button'
+ './Button':'./src/Button'
}
});
Uncaught TypeError: fn is not a function
リモートコンテナが見つからない可能性があります。追加されていることを確認してください。使用しようとしているリモートのコンテナがロードされているにもかかわらず、このエラーが表示される場合は、ホストコンテナのリモートコンテナファイルもHTMLに追加してください。
異なるリモートから複数のモジュールをロードする場合は、複数のwebpackランタイム間の衝突を避けるために、リモートビルドのoutput.uniqueName
オプションを設定することをお勧めします。