在微前端架构中,Web Components 与模块联邦是两个常被混淆的概念。许多人认为它们都能实现“跨应用共享”,于是考虑换个封装形式即可。然而实际上,这两者所要解决的问题层次截然不同。
首先需要明确一个关键区别:Web Components 本质上属于浏览器原生的封装机制,其职责仅限于将自定义标签渲染至页面,而内部逻辑则各自独立。相比之下,模块联邦的核心优势在于运行时依赖协商能力。例如,当两个微应用各自打包了一份 React 库时,模块联邦可通过 shared 配置强制只加载一个共享实例。然而,Web Components 并不具备这一机制。
因此,将 Web Components 直接套用 Module Federation 的思路,很容易陷入误区。
Web Components 并非模块联邦的替代方案
许多开发者在看到 customElements.define() 与 import('./remote.js') 都能加载代码时,便默认两者等同。但关键区别在于:Web Components 既不处理依赖版本,也不参与 shared scope 协商,更不具备 singleton 控制能力。
- React/Vue 组件在跨应用复用场景下,若两个微应用使用了不同版本的
react,Web Components 无法自动对齐版本——它只负责渲染自定义标签,内部逻辑依然各自打包各自的 React 库 - 诸如
shared: { react: { singleton: true } }的配置,在 Webpack Module Federation 中有效,但在纯 Web Components 场景下则完全不起作用 - 缺少
remoteEntry.js的加载流程,也就没有初始化时的init(sharedScope)步骤,依赖冲突只能依靠人工规避
HTML 模块联邦 ≠ Web Components + import()
所谓“HTML 微模块联邦”,通常被误解为直接在 HTML 中引用 ,再使用 。这看似实现了解耦,实则暗藏三个硬伤:
- 资源加载缺乏优先级控制:
标签为并行加载,无法像 Module Federation 那样通过eager: true配置强制提前加载核心依赖(例如lit或react) - 样式隔离依赖 Shadow DOM,但全局 CSS 变量、字体、图标字体等仍可能产生冲突,而且无法像
shared: { 'antd': { singleton: true } }那样统一注入共享依赖 - 缺少运行时错误兜底机制:远程模块加载失败时,
将呈现为空白节点,而 Module Federation 至少能抛出Container not available错误供开发者捕获
真要基于 Web Components 做联邦共享,必须补足缺失层
若坚持使用 Web Components 构建微前端,同时希望获得类似 Module Federation 的依赖治理能力,则需要自行实现以下三项工作:
- 手动管理依赖生命周期:通过
import('https://cdn.com/react@18.2.0.js')加载,配合window.React检查以及Object.assign(window, { React })注入,模拟singleton行为 - 暴露可协商的版本接口:远程模块需要提供
__MFE_VERSIONS__ = { react: '^18.2.0', lit: '^3.0.0' }全局变量,主应用在加载前进行版本比对 - 封装加载器以替代原生
:使用import('./loader.js').then(m => m.loadRemote('mfe-b')),并支持 fallback、timeout、retry 等机制,语义上接近get('./Component')
这些补丁累加起来,代码量与维护成本远高于直接使用 Webpack Module Federation;况且,如果项目中已经采用 React/Vue,再额外用 Web Components 封装一层,反而会增加抽象层级与调试复杂度。
真正容易被忽略的是:Web Components 解决的是“封装粒度”问题,而 Module Federation 解决的是“依赖拓扑”问题。两者目标不同,强行合并时,最容易遇到版本协商失败却无日志提示的情况,或者 Shadow DOM 内部调用的 useState 来自不同 React 实例而引发 Hook 报错——这种错误不会在构建阶段出现,只会在用户点击按钮后才被触发。
