Dynamically load remoteEntry.js files

Alex O'Callaghan

remoteEntry.js ファイルを動的に読み込む

原文は Alex O'Callaghan により に公開されました。 このブログを購読する

Webpack Module Federationは、マイクロフロントエンドアーキテクチャのWebアプリケーションを実現するために利用できます。独立してデプロイされたリモートアプリケーションは、remoteEntry.jsファイルを読み込むことでロードできます。これらのファイルを動的に読み込むことで、パフォーマンスやキャッシュをより柔軟に制御できるほか、A/Bテストやブランチごとのリリースといった機能も実現できます。

Mintelでは、独立した自律的なチームが保守する個別のアプリケーションを読み込む、シェルとなる「ホスト」アプリケーションという形でModule Federationを活用しています。このアプローチに至った経緯の詳細については、"Adopting a micro-frontend architecture"をご覧ください。

デフォルトでは、Webpack Module Federationはすべてのリモートに対してremoteEntry.jsファイルを読み込みます。チームが新しいアプリケーションを開発するにつれて、ホストが抱えるリモートの数は増えていきました。しかし、これらすべてを一度に読み込む必要はありません。各アプリケーションは独立して動作し、ユーザーはほかのアプリケーションに遷移することなく、一つのアプリケーション上で作業を完結させることが多いからです。これほど多くのremoteEntry.jsファイルを読み込むことが、アプリケーションの初期ロード時間に影響を与えていました。

Dynamic Remote Containersのアプローチを使えば、remoteEntry.jsファイルの読み込みを制御できます。

ModuleFederationPluginの設定からremotesフィールドを削除します。

plugins: [
  new ModuleFederationPlugin({
    name: "host-app",
    remotes: {},
  }),
];

独自のコードで<script>タグを挿入して、remoteEntry.jsを手動で読み込みます。

const remoteEntryUrl = "https://something/remote-app/remoteEntry.js";
const scope = "remote-app";
const moduleName = "App";

await __webpack_init_sharing__("default");

await new Promise<void>((resolve, reject) => {
  const element = document.createElement("script");

  element.src = remoteEntryUrl;
  element.type = "text/javascript";
  element.async = true;

  element.onload = () => {
    element.parentElement?.removeChild(element);
    resolve();
  };

  element.onerror = (err) => {
    element.parentElement?.removeChild(element);
    reject(err);
  };

  document.head.appendChild(element);
});

// Initialize the federated module
const container: any = window[scope as any];
await container.init(__webpack_share_scopes__.default);

// Fetch module exposed by the federated module
const factory = await container.get(moduleName);
return factory();

module-federation-import-remoteパッケージを使えば、このロジックを簡単に実装できます。

import { importRemote } from "module-federation-import-remote";

importRemote({ url: "https://something/remote-app", scope: "remote-app", module: "App" }).then((App) => {...});

// If App is a React component you can use it with lazy and Suspense just like a dynamic import:
const App = lazy(() => importRemote({ url: "https://something/remote-app", scope: "remote-app", module: "App" }));

return (
  <Suspense fallback={<div>Loading App...</div>}>
    <App />
  </Suspense>
);

このアプローチにより、Reactコンポーネントが実際にレンダリングされる必要が生じるまでremoteEntry.jsの読み込みを遅延させることで、初期ロードのパフォーマンスを改善できました。リモートの読み込み方法をこのように制御することで、たとえばフィーチャーフラグやユーザーの属性に基づいて、実行時に異なるremoteEntry.jsファイルを動的に読み込むことも可能になります。

importRemoteはデフォルトでremoteEntry.jsのURLにキャッシュバスティング用のクエリパラメータを付与し、これらのエントリーポイントがキャッシュされないようにします。クエリ文字列によるキャッシュバスティングには限界があることに注意してください。このGitHubスレッドでも議論されているように、CDNの設定やリモートのバージョン管理を支援するサービスの導入についても検討することをお勧めします。

さらに読む:

この記事は「muse-spark-1.2-contributor」を使用して翻訳されました。

コメント