- 示例工程
【免费下载链接】module-federation-examples
Implementation examples of module federation , by the creators of module federation
导读
本指南围绕 module-federation-examples 仓库中的 isolate-shared-dependencies 示例 展开,讲解如何在 Module Federation 场景下做到"共享依赖的代码、但隔离依赖的状态"——通过一个可组合的 runtime plugin,在多个容器按父子层级加载且几乎不互相交互时,既节省网络流量与加载耗时,又规避 Vue 2.x 这类单例模式库因全局状态导致的冲突。读完本文,你将掌握该 runtime plugin 的配置写法、隔离原理(resolveShare 钩子 + webpack require 缓存技巧)以及三个应用演示的完整运行方式,并了解如何用 Playwright 端到端验证隔离行为是否符合预期。
问题背景:共享依赖意味着共享状态
Module Federation 的shared机制在多个容器间复用依赖实例时,默认共享的是"代码 + 实例状态"。绝大多数场景下这是理想行为——各应用共用同一个 React 或 Vue 运行时,避免重复下载。但共享状态在某些库上会带来麻烦:
- 依赖单例模式的库:例如 Vue 2.x 会暴露一个全局实例,当多个容器都在该实例上注册全局组件时,会产生命名冲突或状态互相污染;
- 父子层级加载场景:多个容器按父 → 子关系依次加载,彼此之间几乎没有交互,此时共享状态不仅无用,反而有害,而共享代码却非常有价值——可以显著减少网络占用、降低加载耗时。
isolate-shared-dependencies 示例正是对这一矛盾的 PoC(概念验证)实现:通过一个 runtime plugin,让每个容器可以精确指定哪些依赖需要隔离,被隔离的依赖仍然共享同一份代码产物(chunk),但每个容器拿到的是各自独立的实例。
核心特性
该示例在 README.md 中明确了三点特性:
- 共享代码但不共享状态:减少网络用量与加载耗时;
- 可精确指定要隔离的依赖:通过插件参数声明隔离名单;
- 所有容器都必须启用该插件:即使某个容器不隔离任何依赖(保持默认共享行为),也必须注册插件,否则会抛出 warning,且依赖无法被正确隔离。
第三条是易被忽略的硬性约束:插件的隔离机制依赖每个容器的__webpack_require__缓存快照,缺了任何一个容器的配合,隔离都会静默失效。
示例架构与角色分工
示例由 5 个部分组成,通过 pnpm workspace 组织在 runtime-plugins/isolate-shared-dependencies 目录下:
| 组件 | 角色 | 行为 |
|---|---|---|
| app1 | 父应用,第一个加载 | 共享 vue + shared-lib + shared-lib-2,不隔离任何依赖,使用共享实例;加载 app2 |
| app2 | 子应用,第二个加载 | 隔离 shared-lib(不使用共享实例);加载 app3 |
| app3 | 子应用,最后加载 | 同 app1,不隔离依赖,应与 app1 共享实例 |
| shared-lib | 共享库 | 实例化时生成随机 ID 并保留,用于直观判断每个应用跑的是哪个实例 |
| shared-lib-2 | 共享库 | 被 shared-lib 传递使用、也被所有应用直接使用,同样生成随机 ID 标识实例 |
应用之间的加载链路是 app1 → app2 → app3,形成一个三级父子嵌套。三个 Vue 应用分别在 3001、3002、3003 端口提供服务,页面上一秒刷新一次实例 ID,方便肉眼观察实例归属。
shared-lib 与 shared-lib-2:如何"暴露"实例
两个共享库的实现非常精简,核心是模块加载时生成一次随机 ID,之后一直复用:
- shared-lib/index.js:模块顶层
const instanceId = getRandomInt(100000)生成 ID,导出getLib1InstanceId();同时内部 import 了 shared-lib-2 并透传getLib2InstanceIdThroughLib1(),用于演示传递依赖场景; - shared-lib-2/index.js:同样在模块顶层生成 ID,导出
getLib2InstanceId()。
关键点在于:ID 是在模块工厂执行(实例化)时生成的。如果多个应用共享同一个模块实例,它们拿到的 ID 必然相同;如果某个应用被隔离,它的模块工厂会独立再执行一次,于是拿到一个不同的 ID。这正是演示与测试判断"是否共享实例"的依据。
插件核心:plugin/isolatePluginFactory.ts 逐行拆解
插件的完整实现在 plugin/isolatePluginFactory.ts,依赖@module-federation/runtime(本仓库锁定 2.5.1,见 plugin/package.json)。它返回一个标准FederationRuntimePlugin对象,只实现了两个钩子:beforeInit与resolveShare,与 runtime-plugins/README.md 中 Hook Coverage 表格的描述一致。
工厂函数与参数解析
export default function IsolateSharedDependenciesPluginFactory( options: string[] | IsolateSharedDependenciesPluginOptions = {}, ): FederationRuntimePlugin { const dependencies = Array.isArray(options) ? options : options.dependencies ?? []; if (!Array.isArray(dependencies)) { throw new Error( 'IsolateSharedDependenciesPlugin needs to receive a list of dependencies to isolate', ); } ... }- 工厂函数支持两种传参形式:直接传字符串数组,或传
{ dependencies: [...] }对象; - 若传入的不是数组,会立即抛错,防止配置失误被静默吞掉;
- 内部用
dependenciesToIsolateSet = new Set(dependencies)保存隔离名单,并维护patchedInstances缓存已隔离依赖的实例。
beforeInit:保存容器标识与 webpack require
beforeInit: args => { originId = args.origin.name; (args.origin as ExtendedFederationHost).__webpack_require__ = __webpack_require__; return args; },每个容器初始化时,插件把当前容器的名称记录为originId,并把该容器编译期注入的__webpack_require__挂到 FederationHost 实例上。这一步是后续"清空模块缓存、重新实例化"的前提——它拿到了该容器 webpack 运行时模块缓存的访问句柄。
resolveShare:隔离逻辑的核心
resolveShare: args => { if (!dependenciesToIsolateSet.has(args.pkgName)) { return args; } const resolvedDependency = args.resolver(); if (!resolvedDependency) { return args; } args.resolver = () => ({ ...resolvedDependency, scope: [originId], lib: undefined, loaded: false, loading: new Promise(async resolve => { const originalFactory = await resolvedDependency.get(); resolvedDependency.lib = originalFactory; resolvedDependency.loaded = true; resolve(() => { if (!patchedInstances[args.pkgName]) { const originInstance = args.GlobalFederation.__INSTANCES__.find( instance => instance.name === resolvedDependency.from, ) as ExtendedFederationHost; ... } return patchedInstances[args.pkgName]; }); }), }); return args; },resolveShare是 Module Federation runtime 的 share 策略钩子,负责为每个依赖产出"resolver"(解析器)。插件的处理逻辑可分四步理解:
- 命中隔离名单才处理:
args.pkgName不在dependenciesToIsolateSet中时原样返回,依赖走默认共享路径; - 改造 resolver:命中时调用原始
args.resolver()拿到共享描述对象,然后返回一个副本:scope限定为[originId](防止与其他容器合并共享作用域)、lib置空、loaded置 false,并重写loadingPromise; - 等待原始模块工厂:
await resolvedDependency.get()先取到其他容器已经执行过的模块工厂(保证共享的代码 chunk 只下载一次); - 包装工厂执行:真正
require时,若该依赖尚未隔离过,则执行一段"缓存隔离"技巧(见下节),并把产物存入patchedInstances复用;若其他容器没有注册本插件(缺少__webpack_require__),则打印 warning 并回退为返回原始工厂——这正是 README 第三条特性的代码落点。
隔离的底层技巧:清空 webpack require 缓存再实例化
const originCache = originInstance.__webpack_require__.c; const savedOriginCache = { ...originCache }; Object.keys(originCache).forEach(key => { delete originCache[key]; }); patchedInstances[args.pkgName] = originalFactory(); Object.keys(originCache).forEach(key => { delete originCache[key]; }); Object.keys(savedOriginCache).forEach(key => { originCache[key] = savedOriginCache[key]; });这段代码是插件的"魔术"所在,思路非常直接:
- 找到第一个容器(即 resolvedDependency 的提供方
from)的__webpack_require__.c模块缓存表; - 把缓存快照保存下来,然后临时清空全部缓存;
- 调用
originalFactory()重新执行模块工厂——由于缓存已被清空,模块会重新初始化,生成全新的实例 ID,也就是"隔离出的独立实例"; - 紧接着再次清空缓存(防止重执行产生的中间产物残留),最后把快照完整还原,确保其他模块的缓存不受影响。
整个操作发生在同一 tick 内同步完成,因此对运行时其他模块是透明的:代码 chunk 只加载一次,但被隔离依赖的实例被"复制"出了一份独立的。
三份 webpack 配置:如何声明隔离名单
三个应用都基于@module-federation/enhanced的 ModuleFederationPlugin 构建,差异仅在name、端口与runtimePlugins参数上。
app1/webpack.config.js(父应用,不隔离):
new ModuleFederationPlugin({ experiments: { asyncStartup: true }, name: 'app1', shareStrategy: 'loaded-first', filename: 'remoteEntry.js', manifest: false, shared: ['vue', 'shared-lib', 'shared-lib-2'], exposes: { '.': './src/index.ts' }, runtimePlugins: [[require.resolve('../plugin/isolatePluginFactory.ts'), { dependencies: [] }]], })app2/webpack.config.js(子应用,隔离 shared-lib):
runtimePlugins: [[require.resolve('../plugin/isolatePluginFactory.ts'), { dependencies: ['shared-lib'] }]],app3/webpack.config.js(子应用,不隔离):
runtimePlugins: [[require.resolve('../plugin/isolatePluginFactory.ts'), { dependencies: [] }]],需要留意的配置要点:
- runtime plugin 元组形式:
runtimePlugins数组的每一项是[插件模块路径, 插件选项]元组,插件选项会作为工厂函数参数传入。这是 runtime plugin 的标准传参方式,也是 runtime-plugins/README.md 中所说的 "tuple runtime plugin parameters"; - 三个应用必须都注册插件:app1、app3 虽传
{ dependencies: [] },但依然注册了插件,否则 warning 出现且隔离失效; shareStrategy: 'loaded-first':保证共享解析时优先复用先加载容器的实例,是隔离逻辑得以成立的基础策略;exposes: { '.': './src/index.ts' }:每个应用对外暴露自己的挂载函数,由父应用通过loadRemote动态加载后调用,见 app1/src/index.ts 导出的mount({ parentContainer })。
页面如何呈现实例 ID:App.vue 的观察窗口
三个应用的 App.vue 结构一致,模板渲染三段信息:
<span>Lib 1 instance ID: {{ sharedLib1Id }} | </span> <span>Lib 2 instance ID through lib 1: {{ sharedLib2IdThroughLib1 }} |</span> <span>Lib 2 instance ID: {{ sharedLib2Id }}</span>data()初始化时分别调用getLib1InstanceId()、getLib2InstanceIdThroughLib1()、getLib2InstanceId()取值,并设置setInterval每 1000ms 刷新一次(见 app1/src/App.vue),这样运行时实例归属一目了然:
- app1 与 app3 不隔离,二者显示相同的 Lib1 / Lib2 ID(共享实例);
- app2 隔离了 shared-lib,其 Lib1 ID 与 app1/app3 不同,但 Lib2(shared-lib-2 未被隔离)仍与 app1/app3 一致;
- 每个应用内 "Lib 2 instance ID through lib 1" 与 "Lib 2 instance ID" 始终相等,说明传递依赖与直接依赖解析到同一个 shared-lib-2 实例。
父子加载通过 runtime 的init+loadRemote实现:app1 在 mounted 中init注册 remote app2 并loadRemote('app2')后调用其mount(app1/src/App.vue),app2 同样加载 app3(app2/src/App.vue),app3 则不再加载子应用。
运行演示与验证
启动三个应用
在 runtime-plugins/isolate-shared-dependencies 目录下执行:
pnpm run start该命令(见 package.json 的pnpm --filter isolate-shared-dependencies_app* --parallel start)会并行构建并启动 app1、app2、app3,分别监听:
- http://localhost:3001/ (app1,父应用)
- http://localhost:3002/ (app2,子应用)
- http://localhost:3003/ (app3,子应用)
打开 3001 端口页面即可看到三个应用嵌套渲染并实时显示各自的实例 ID。
端到端验证:Playwright 测试
仓库自带完整的 Playwright 测试 e2e/checkIsolatedDependencies.spec.ts,测试配置见 playwright.config.ts(基于 runtime-plugins/playwright.base.ts 的createRuntimePluginPlaywrightConfig生成,默认 baseURL 指向 3001)。运行方式:
pnpm run test:e2e测试对"隔离是否真的生效"做了精确断言:
- 三个应用都成功加载(
App 1/2/3 loaded可见); - app2 的 Lib1 ID 与 app1 不同,证明 shared-lib 被隔离出独立实例;
- app3 的 Lib1/Lib2 ID 与 app1 相同,证明不隔离的应用仍共享实例;
- 每个应用内部 Lib2 直接与透传 ID 一致,证明 shared-lib-2 的传递依赖解析正确。
其中extractInstanceId通过正则/\d+/从页面文本中抽取数值并比较,是验证共享/隔离行为的核心断言逻辑(见 checkIsolatedDependencies.spec.ts)。
实现要点总结与适用边界
- 适用场景:多个容器以父子关系加载、彼此几乎不交互,但希望复用依赖的代码产物以降低网络与加载成本;或使用了 Vue 2.x 这类依赖全局单例的库,需要规避全局状态冲突;
- 实现路径:
beforeInit保存容器标识与__webpack_require__,resolveShare命中隔离名单后改写 resolver,通过临时清空并还原首个容器的模块缓存,强制模块工厂重新执行以得到独立实例; - 硬性前提:所有参与容器都必须注册本插件,且依赖的提供方必须先行加载(
loaded-first策略),否则插件会打印[IsolateSharedDependenciesPlugin] ... can't isolate ...形式的 warning 并回退为共享实例; - 限制说明:该实现是官方仓库中的 PoC(概念验证)示例,仓库 README 也标注了部分功能 "Not implemented yet",生产环境接入前应结合自身依赖图评估其适用性。
若想进一步探索 runtime plugin 体系,可继续阅读 runtime-plugins 目录,其中展示了beforeRequest、errorLoadRemote、createScript、beforePreloadRemote等更多钩子的用法,以及 control-sharing、remote-router 等其他策略型插件示例。
- 示例工程
【免费下载链接】module-federation-examples
Implementation examples of module federation , by the creators of module federation
相关推荐
深入解析 WAMR 共享堆(Shared Heap):多 WASM 实例零拷贝数据共享实战
深入解析 WAMR 共享堆(Shared Heap):多 WASM 实例零拷贝数据共享实战 导读 共享堆(Shared Heap)是 WebAssembly M
可观测性日志分析云原生流处理终极指南:Webpack Module Federation 跨应用代码共享实践
终极指南:Webpack Module Federation 跨应用代码共享实践 Webpack Module Federation 是现代前端工程化中实现跨应
前端示例工程前端构建如何3步掌握LangChain:新手也能快速上手的AI代理开发完整教程
如何3步掌握LangChain:新手也能快速上手的AI代理开发完整教程 LangChain是当前最流行的AI代理工程平台,它让开发者能够轻松构建基于大语言模型的
人工智能大模型AI AgentAgent 框架RAG
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考