写过前端的人都踩过这个坑——微前端每次配共享依赖都是版本战争,今天 Module Federation 2.0 把这件事彻底原生化了

写过前端的人都踩过这个坑——微前端配共享依赖,host 用 React 18、remote 用 React 17,单例模式一开浏览器直接两个 React 实例打架。今天这件事被 Module Federation 2.0 的运行时插件彻底原生化了。

共享依赖版本战:微前端的老大难

微前端用 Module Federation 最大的坑,就是共享依赖版本不统一。host 声明了 react@^18.0.0,remote 打包时用的是 react@^17.0.0,两套 React 同时跑进浏览器——hooks 报错、状态错乱,严重时整个页面白屏。

以前的解法:要么强制限死 requiredVersion: ^18.0.0 逼 remote 升级,要么手动写一堆 fallback 代码兜底,要么直接放弃单例模式接受两份 bundle。这三种解法各有各的难受。

Module Federation 2.0 给了第三种路:运行时插件,让你在加载远程模块的时候动态改共享依赖的选用策略。

resolveShare:运行时接管版本仲裁

resolveShare 是 MF 2.0 运行时插件的核心 hook,它在每次模块加载时触发,你可以在此时覆盖版本选择逻辑:

import type { ModuleFederationRuntimePlugin } from @module-federation/enhanced/runtime;

function preferHostReact(): ModuleFederationRuntimePlugin {
  return {
    name: prefer-host-react,
    resolveShare(args) {
      if (args.pkgName !== react) return args;

      // 强制 host 的版本作为仲裁者
      const hostMap = args.shareScopeMap.default?.react;
      if (!hostMap) return args;

      const preferred = hostMap[args.version] ?? Object.values(hostMap)[0];
      if (!preferred) return args;

      args.resolver = () => ({ shared: preferred, useTreesShaking: false });
      return args;
    },
  };
}

配上这个插件之后,无论 remote 打包时用了什么版本,运行时会直接覆盖为 host 指定的版本。不需要改 remote 的构建配置,不需要发版,一个插件全搞定。

实际效果:host 声明 react@18.2.0 单例,remote 打包时用的是 react@17.x,运行时直接拿 host 的 18.2.0。页面里只有一份 React,两个模块都正常工作。

Tree Shaking 共享依赖:按需加载,只拿用到的

除了版本冲突,另一个长期痛点是共享依赖的体积。Module Federation 的 shared 机制保证同一份依赖只加载一次,但早期实现是「只要 shared 里声明了就整包加载」——实际上 remote 可能只用了一个工具函数,整个 lodash 就被塞进了 shared bundle。

MF 2.0/Rspack 1.5 引入 shared.treeShaking,在运行时按需裁剪:

// rspack.config.mjs
new rspack.container.ModuleFederationPlugin({
  name: host,
  shared: {
    react: {
      singleton: true,
      requiredVersion: ^18.0.0,
      // 开启 tree shaking,只加载实际用到的那部分
      treeShaking: true,
    },
  },
});

加上这个选项之后,构建产物里会多出一个「实际使用图谱」,运行时只实例化 remote 真正引用的那些模块。根据 Module Federation 官方数据,tree shaking 之后 shared bundle 体积能减少 30%-60%,对 lodash、date-fns 这类大库效果尤其明显。

解耦 Runtime:Webpack 和 Rspack 终于可以混用

Module Federation 1.x 的 Runtime 是直接嵌入在 Webpack 里的——remote 必须用 Webpack 打包才能被 host 加载。想迁移到 Rspack 提速?对不起,MF 不支持。

MF 2.0 把 Runtime 抽成了一个独立 SDK @module-federation/enhanced/runtime,彻底与构建工具解耦:

import { createInstance } from @module-federation/enhanced/runtime;

const mf = createInstance({
  name: host,
  remotes: [
    { name: remote_a, entry: http://cdn-a.com/mf-manifest.json },
    // remote_b 可能是 Rspack 打的包,remote_c 是 Rollup 打的
    { name: remote_b, entry: http://cdn-b.com/mf-manifest.json },
  ],
  plugins: [preferHostReact()],
});

const api = await mf.loadRemote("remote_a/ProductCard");

现在 remote 可以用 Webpack 打、Rspack 打甚至 Rollup 打,只要遵循 mf-manifest.json 协议,运行时统一加载。Rspack 1.5 更是直接内置了 MF 1.5,连插件都不用装。

下一步

如果你的项目正在跑微前端,第一件事是检查 shared 配置里的版本策略——把 singleton: truerequiredVersion 配对锁死;第二件事是尝试加一个 resolveShare 插件,让 host 真正成为版本仲裁者。如果是新项目,直接上 Rspack 1.5 + MF 2.0,原生支持 tree shaking 和跨构建工具互操作。MF 2.0 不要求一次性全量迁移,可以按 remote 逐个升级,老项目不受影响。

评论区

0 条评论

登录后可评论。

阿柯·前端架构 11 阅读