写过三年微前端,每次接新模块版本冲突都要靠重启续命——Module Federation 2.0 把这件事彻底变了

写过三年微前端,每次接新模块进来第一件事就是祈祷版本对得上——React 18 对 18.2,错了;antd 4.5 对 5.x,又错了。版本冲突这件事,v1 时代基本靠「重启大法」硬扛,没什么好办法。

Module Federation 2.0 把这件事彻底变了。

旧版本的三个硬伤

Module Federation 最初是 Webpack 5 引入的,思路很清晰:一个 build 可以消费另一个 build 暴露出来的模块,运行时加载,不用 npm 包,不用发版协调。概念上接近后端的微服务。

但 v1 用久了会发现三个绕不开的坑:

共享依赖版本战争。 shared 配置里只要有版本不一致,每个容器各装各的,React 被打进两份 bundle,页面直接 hydration 报错。singleton 模式能压住一部分,但碰上 antd 和它的内部依赖就很容易爆。

remoteEntry.js 太粗糙。 整个容器只暴露一个入口文件,Host 想精确拿某个组件就要靠字符串路径拼过来,没有类型,IDE 全靠 any,refactor 一次炸一次。

没有运行时扩展能力。 想做版本降级、做 A/B 切流、加灰度逻辑,基本只能自己在外面套层代理,侵入性很强。

Manifest 协议:remoteEntry.js 的替代方案

v2 最大的变化之一是把 remoteEntry.js 换成了 mf-manifest.json。

Host 端旧写法:

remotes: {
  checkout: "checkout@https://checkout.example.com/remoteEntry.js",
}

v2 写法:

remotes: {
  checkout: "checkout@https://checkout.example.com/mf-manifest.json",
}

manifest 文件里记录了每个暴露模块的名称、类型、版本要求和依赖信息,不再只是一个 JS 入口。这是 Host 和 Remote 之间的一份结构化契约,更新 Remote 时不用动 Host 配置,生产运维的灵活性大幅提升。

共享依赖 Tree Shaking:按需打包

这是 v2 真正有价值的地方。之前 shared 机制会把整个依赖包打进去——你只用了一个 antd Button,但整个组件库全量进了 bundle。v2 的 shared Tree Shaking 精确到模块导出级别:

// rspack.config.ts
export default {
  plugins: [
    new ModuleFederationPlugin({
      shared: {
        antd: {
          treeShaking: {
            mode: "server-calc", // 推荐模式
          },
        },
      },
    }),
  ],
};

两种模式:runtime-infer 轻量,适合本地开发和单一消费者场景;server-calc 靠中心化服务在部署时汇总所有消费者的 usedExports,构建出全局最优的共享包,antd 这类大库用这个模式效果最明显。

Runtime Plugin:把模块加载变成可编程的

v2 还带来了完整的运行时插件系统,挂在模块加载生命周期上:

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

export default function preferHostReact(): ModuleFederationRuntimePlugin {
  return {
    name: "prefer-host-react",
    resolveShare(args) {
      if (args.pkgName !== "react") return args;
      const hostVersionMap = args.shareScopeMap.default?.react;
      if (!hostVersionMap) return args;
      const preferred = hostVersionMap[args.version] ?? Object.values(hostVersionMap)[0];
      if (!preferred) return args;
      args.resolver = () => ({ shared: preferred, useTreesShaking: false });
      return args;
    },
  };
}

常用 Hook 包括:beforeRequest(模块加载前拦截)、afterResolve(解析后改路径)、fetch(自定义 manifest 请求逻辑,带凭证头)、resolveShare(最终决定用哪个共享版本)、errorLoadRemote(远程加载失败时的兜底)。用 errorLoadRemote 配合 shareStrategy: loaded-first 可以让 Remote 不可用时不立刻炸,而是等到代码真正用到时再报错,体验好很多。

跨模块类型安全

@module-federation/typescript 插件从 Remote 的 manifest 自动生成 .d.ts 文件,Host 在开发时就能拿到完整的类型提示,不再是 any:

// 类型自动推导,IDE 全程提示
import CheckoutCart from "checkout/Cart";
import ProductGrid from "catalog/ProductGrid";

这个在 v1 时代是完全缺失的,v2 补上了这个短板。

迁移建议

已有 Webpack/Rspack 项目迁移成本不高:把 ModuleFederationPlugin 换成 @module-federation/enhanced,把 remoteEntry.js 替换成 mf-manifest.json URL,shared 配置完全兼容旧写法,可以逐个依赖开启 Tree Shaking。

Vite 用户用 @module-federation/vite 插件,Nx 用户用 withModuleFederation() 工具函数,Nx 还内置了 NxRuntimeLibraryControlPlugin 保证 workspace 库在本地开发时正确走共享路线。

下一步

Module Federation 2.0 已经不再只是一个 Webpack 特性——它正在成为大型 Web 应用去中心化架构的方法论。接下来值得关注的是 DevTool 工具链完善、Router/Sandbox/SSR 等框架级能力。

如果你的团队有两个以上的前端模块需要独立部署,v2 这套方案值得现在就跑进生产。

评论区

0 条评论

登录后可评论。

阿柯·前端架构 12 阅读