Module Federation 依赖隔离实战:共享代码却不共享状态(isolate-shared-dependencies 深入解析)
示例工程【免费下载链接】module-federation-examplesImplementation examples of module federation , by the creators of module federation项目地址https://gitcode.com/gh_mirrors/mo/module-federation-examples点击查看免费下载导读本指南围绕 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不隔离任何依赖使用共享实例加载 app2app2子应用第二个加载隔离 shared-lib不使用共享实例加载 app3app3子应用最后加载同 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 requirebeforeInit: 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-libruntimePlugins: [[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 })。页面如何呈现实例 IDApp.vue 的观察窗口三个应用的 App.vue 结构一致模板渲染三段信息spanLib 1 instance ID: {{ sharedLib1Id }} | /span spanLib 2 instance ID through lib 1: {{ sharedLib2IdThroughLib1 }} |/span spanLib 2 instance ID: {{ sharedLib2Id }}/spandata()初始化时分别调用getLib1InstanceId()、getLib2InstanceIdThroughLib1()、getLib2InstanceId()取值并设置setInterval每 1000ms 刷新一次见 app1/src/App.vue这样运行时实例归属一目了然app1 与 app3 不隔离二者显示相同的 Lib1 / Lib2 ID共享实例app2 隔离了 shared-lib其 Lib1 ID 与 app1/app3 不同但 Lib2shared-lib-2 未被隔离仍与 app1/app3 一致每个应用内 Lib 2 instance ID through lib 1 与 Lib 2 instance ID 始终相等说明传递依赖与直接依赖解析到同一个 shared-lib-2 实例。父子加载通过 runtime 的initloadRemote实现app1 在 mounted 中init注册 remote app2 并loadRemote(app2)后调用其mountapp1/src/App.vueapp2 同样加载 app3app2/src/App.vueapp3 则不再加载子应用。运行演示与验证启动三个应用在 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] ... cant isolate ...形式的 warning 并回退为共享实例限制说明该实现是官方仓库中的 PoC概念验证示例仓库 README 也标注了部分功能 Not implemented yet生产环境接入前应结合自身依赖图评估其适用性。若想进一步探索 runtime plugin 体系可继续阅读 runtime-plugins 目录其中展示了beforeRequest、errorLoadRemote、createScript、beforePreloadRemote等更多钩子的用法以及 control-sharing、remote-router 等其他策略型插件示例。赞分享示例工程【免费下载链接】module-federation-examplesImplementation examples of module federation , by the creators of module federation项目地址https://gitcode.com/gh_mirrors/mo/module-federation-examples点击查看免费下载相关推荐深入解析 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上一篇uni-app x 鸿蒙HarmonyOS断点调试完全指南uts / uvue / ets 调试与联编调试实战下一篇基于 QMK 固件打造 Cluecard RGB 灯光效果测试键位原理、配置与 HID 调试实战创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考