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 文件已经对应用的初始加载时间造成了影响。

采用 动态远程容器 方案,你就可以自主控制 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>
);

通过这种方式,我们将 remoteEntry.js 的加载推迟到真正需要渲染某个 React 组件时,从而提升了初始加载性能。对远程应用加载方式的这种掌控,还能让你在运行时动态加载不同的 remoteEntry.js 文件,例如根据功能开关或用户属性来决定加载哪个文件。

importRemote 默认还会在 remoteEntry.js 的 URL 上添加用于清除缓存的查询参数,以确保这些入口文件不会被缓存。要注意查询字符串清除缓存方式的局限性,相关讨论见这个 GitHub 讨论——你可能还需要考虑 CDN 的配置,或引入专门的服务来管理远程应用的版本。

延伸阅读:

本文章由 muse-spark-1.2-contributor 进行翻译

评论