广告位 · 页头(后台-模块管理-新建模块后替换此区域)

Webpack 5 的 Faithful Universe 是什么?如何用它构建稳定微前端?

Webpack 5 的模块联邦(Module Federation)发布后,前端应用的组合方式发生了明显变化。宿主应用可以在运行时通过远程入口加载另一个应用暴露出来的模块,远程模块甚至能复用宿主已经加载的 React、Vue、Angular 等库。Faithful Universe 忠诚宇宙这个说法,正是对这套协作体系的一种形象概括:所有独立构建的应用共同组成一个运行宇宙,而共享依赖的版本承诺就是维持这个宇宙稳定的引力。如果某个远程应用私自带入了一份版本不同的依赖,轻则出现上下文断裂,重则整个页面直接白屏。

Webpack 5 的 Faithful Universe 是什么?如何用它构建稳定微前端?

本文围绕 Webpack 5 中与忠诚宇宙直接相关的几项能力展开,包括模块联邦的 shared 共享作用域、持久化缓存和确定性模块 ID,以及实际工程里防止依赖漂移的具体措施。通过完整配置示例和常见错误对比,你可以快速理解为什么单例约束、版本声明和运行时隔离在微前端场景里缺一不可。

共享作用域:忠诚宇宙的引力规则

模块联邦中的 shared 配置并不是简单地把某个依赖从 bundle 里剔除,而是在所有参与方之间创建一个共享作用域。宿主应用和远程应用都会优先从这个作用域中获取依赖实例。Webpack 5 为 shared 提供了几个关键字段:singleton 表示整个运行时只允许存在一个该依赖的实例;requiredVersion 声明可接受的版本范围;eager 决定是否在入口处立即加载共享依赖;strictVersion 则在版本不匹配时直接报错,而不是静默降级。

在一个典型的 React 微前端场景中,如果主应用和远程应用各自打包了一份 React,那么页面里就会同时存在两个 React 实例。React 的 hooks 机制依赖全局的 ReactCurrentOwner 和 dispatcher 状态,两个实例会导致 useContext、useState 等 API 出现 Invalid hook call 错误。把 react 和 react-dom 声明为 singleton 后,Webpack 会确保整个运行时只加载一次该依赖,远程模块会复用宿主已经加载的那一份。

const ModuleFederationPlugin = require('webpack/lib/container/ModuleFederationPlugin');
module.exports = {
  mode: 'development',
  entry: './src/index.js',
  plugins: [
    new ModuleFederationPlugin({
      name: 'hostApp',
      remotes: {
        productApp: 'productApp@http://localhost:3001/remoteEntry.js'
      },
      shared: {
        react: { singleton: true, requiredVersion: '^18.2.0', eager: false },
        'react-dom': { singleton: true, requiredVersion: '^18.2.0', eager: false }
      }
    })
  ]
};

上面这段配置中,宿主应用声明了两个远程入口,并把 react 和 react-dom 设置为单例共享。eager 设为 false 表示这些依赖不会在宿主入口里被迫加载,而是在远程模块真正引用时才从共享作用域中获取。这样做的好处是,宿主应用不会因为某个远程模块暂时未被访问而提前承担依赖加载成本。但要注意,如果远程模块在初始化阶段就同步引用了 react,就需要考虑把 eager 打开,否则可能遇到依赖尚未就绪的问题。

远程应用的 shared 声明同样重要。很多团队只在宿主配置里写了 shared,远程应用却仍然把 react 打包进自己的产物里。结果就是宿主和远程各自拥有一份 react,singleton 约束失效。正确的做法是远程应用也声明相同的 shared 字段,让 Webpack 在远程构建时把共享依赖排除出去,并在运行时从宿主提供的共享作用域中获取。

持久化缓存与确定性模块 ID:让忠诚状态可复现

依赖版本的一致性只是忠诚宇宙的一个侧面,模块标识的稳定性同样不可忽视。Webpack 5 默认启用了持久化缓存,能够把模块处理结果保存到磁盘,下次构建时跳过大量重复工作。但缓存有效的前提是模块 ID 不能随意变化。早期 Webpack 默认按模块解析顺序分配数字 ID,插入或删除一个模块,后续所有模块的 ID 都会跟着变。对于模块联邦来说,远程应用暴露的 chunk 文件名和内部模块 ID 一旦漂移,宿主应用可能依然请求到旧的 chunk 入口,却无法正确对齐共享作用域里的依赖关系。

Webpack 5 提供了 deterministic 模块 ID 生成策略。它会根据模块在项目中的相对路径计算出一个稳定的数字或字符串 ID,不受构建顺序影响。配合 chunkIds 的确定性设置,每次构建产出的 chunk 名称和模块标识都能保持一致。这对远程应用的缓存命中和增量部署非常关键。

module.exports = {
  output: {
    chunkLoadingGlobal: 'webpackChunk_hostApp'
  },
  optimization: {
    moduleIds: 'deterministic',
    chunkIds: 'deterministic',
    runtimeChunk: 'single'
  }
};

chunkLoadingGlobal 用来定义全局变量名,多个应用同时运行时不会因为 Webpack 运行时互相覆盖而出错。runtimeChunk 设为 single 会把 Webpack 的运行时逻辑单独拆成一个文件,避免宿主和远程应用各自携带一份运行时导致全局变量冲突。可以把这个配置理解成忠诚宇宙里每个星球的唯一通信频段,频段不重叠,信息才能正确传递。

除了模块 ID,持久化缓存还影响远程入口文件的 hash 计算。如果远程应用更新了与共享依赖无关的代码,但 moduleIds 配置不稳定,远程入口可能会有大量 chunk 一起失效。通过确定性 ID 和稳定的 chunkLoadingGlobal,远程应用可以只更新真正变化的那部分文件,减少宿主应用侧的加载成本。

工程实践:如何避免依赖漂移

依赖漂移最常见的来源有三个:一是 package.json 里的版本范围写得太宽,导致不同时间安装的依赖版本不一致;二是远程应用为了本地开发方便,把共享库也打进了远程包;三是 shared 配置缺少 strictVersion,版本不匹配时只是控制台警告,没有在构建阶段暴露问题。

第一件事是锁定关键依赖版本。对于 React、Vue、Angular 这类运行时共享库,建议在 package.json 中使用精确版本或很窄的范围,并且把 lockfile 提交到版本库。如果宿主和远程应用在不同的仓库里维护,可以抽出一个公共的依赖清单文件,在 CI 构建时用脚本比对关键依赖版本。版本不一致不是不能运行,而是会大大增加运行时调试成本。

第二件事是让远程应用也坚持声明 shared,并把共享依赖从产物中排除。下面是一个远程应用的推荐配置,和宿主应用保持相同的 shared 定义。

const ModuleFederationPlugin = require('webpack/lib/container/ModuleFederationPlugin');
module.exports = {
  entry: './src/main.js',
  plugins: [
    new ModuleFederationPlugin({
      name: 'productApp',
      filename: 'remoteEntry.js',
      exposes: {
        './ProductList': './src/components/ProductList'
      },
      shared: {
        react: { singleton: true, requiredVersion: '^18.2.0', strictVersion: true },
        'react-dom': { singleton: true, requiredVersion: '^18.2.0', strictVersion: true }
      }
    })
  ]
};

这里把 strictVersion 设为 true,表示远程应用在构建时会检查宿主提供的版本是否满足 requiredVersion。如果不满足,构建会直接失败,而不是在运行时暴露出难以追踪的 hooks 错误。对于已经上线的应用,可以在浏览器控制台里执行 Object.keys(window) 查看是否存在多个 webpackChunk 变量,或者通过 __webpack_share_scopes__ 查看当前共享作用域里的依赖版本。出现多个同名依赖实例时,基本可以确定是某个远程应用没有正确配置 shared。

还有一种情况是本地开发环境与生产环境不一致。远程应用单独启动时,如果宿主没有运行,它就需要自己加载共享依赖。这时可以把 shared 中的 eager 临时设为 true,或者使用不同环境的 Webpack 配置文件。生产构建时再恢复为交给宿主加载,这样既能保证远程应用独立开发体验,又不会破坏忠诚宇宙的运行时约束。

Faithful Universe 忠诚宇宙不是 Webpack 5 某个单独的 API,而是一种对模块联邦、共享作用域和持久缓存协同工作的工程化表达。它的核心在于两点:依赖实例的唯一性和模块标识的稳定性。只要把 shared 的 singleton、requiredVersion、strictVersion 用好,同时在 CI 中锁定依赖版本,远程应用就能像本地模块一样可靠地运行在宿主宇宙中。

广告位 · 文章底部(后台-模块管理替换此区域)