
1. 为什么一个框架要开放两个入口从单实例到多实例的工程演进我在正式聊 registerMicroApps 和 loadMicroApp 的对比之前想先把一个很容易被忽略的背景讲透Qiankun 为什么非要多此一举提供两个加载 API而不是一个 API 打天下最早接触 Qiankun 的同学大概率都是从官方文档里那句微前端的核心在于拆分与自治开始的。注册式 APIregisterMicroApps看起来要更正统——它像 single-spa 那样基于路由规则来自动匹配、自动挂载、自动卸载一套生命周期全托管。只要你把子应用列表和 activeRule 配好剩下的交给框架。这种模式天然契合主应用壳 若干业务子应用的经典中后台形态菜单、权限、布局全在主应用子应用按路由去激活互不干扰。但实际进到项目里你很快会发现不是所有场景都长这样。有些团队只是想把一个特别重的独立页面比如报表、低代码编辑器、大规模数据可视化屏嵌进主应用这个页面有自己的路由体系、自己的菜单、自己的构建部署流程它根本不该被主应用的路由规则绑架。这个时候如果用 registerMicroApps 去注册你得给它单独划一个 activeRule还得处理子应用内部路由跟主应用路由的嵌套关系麻烦不说还容易把两套路由机制搅在一起。于是 loadMicroApp 这个手动加载 API 就成了第二选择它不依赖路由匹配你指定一个 DOM 容器节点传入子应用的 name、entry它就立刻在那个节点里把子应用挂起来返回给你一个包含 mount/unmount/unload 方法的实例。挂载、卸载、销毁全由业务代码自己控制自由度一下子拉满。一句话概括registerMicroApps 是开箱即用但绑死路由loadMicroApp 是自由加载但一切自己管。两种 API 不是替代关系而是 Qiankun 为了覆盖两级工程需求故意做出来的双入口。理解了这个背景后面所有对比细节才有着力点。2. registerMicroApps 与 loadMicroApp 的底层差异路由绑定与沙箱隔离机制下面这张表是我在实践中总结出来的核心差异对照建议收藏。后面每个点我都会单独展开。对比维度registerMicroAppsloadMicroApp挂载触发方式由主应用路由变化自动触发手动调用 API传入容器节点路由控制权由 activeRule 统一接管由业务代码自行决定何时加载/卸载配置入口一次性注册所有子应用每次挂载都要传单例配置子应用数量一次注册多个按规则逐一激活一个 loadMicroApp 调用只挂一个子应用生命周期把控依赖框架统一调度完全由调用方掌握时序沙箱隔离方式基于运行时快照或代理的沙箱方案默认沙箱策略与注册模式一致但入口参数可微调预加载支持支持 prefetchAfterCreation 自动预加载支持配置 prefetch 属性但更适合手动按需加载典型适配场景中后台多模块整体集成独立工具页/低代码编辑器/第三方单页嵌入2.1 路由绑定自动连接器的黑魔法与手动模式的自由落体registerMicroApps 自动路由匹配的实现本质上是 ReactRouter 思路的微前端版本。主应用启动时传入routes数组每个路由对象的activeRule会在地址变化时被逐一校验。命中则触发 load、bootstrap、mount 这一串生命周期离开则触发 unmount。这套机制的好处是主应用只需要维护一张表不用在代码里写什么时候该挂谁的业务判断。但代价也在这你的子应用路由必须和主应用路由保持强一致。比如 list 子应用注册了/list规则它内部如果有/list/detail这样的二级路由主应用必须能够把这段地址完整传下去。很多团队在这过第一个坑主应用用的是 hash 路由子应用用的是 browser 路由或者反过来结果子应用加载出来之后地址栏的 URL 对不上导致子应用内部路由一直落在首页。这在注册模式下排查起来很费劲因为问题发生在框架的自动调度层你只能靠调试路由对象一步步定位。loadMicroApp 就没有这层顾虑了。它只要拿到一个容器 DOM 节点比如div idcontainer/div传入{ name: reports, entry: //xxx/reports }子应用就会在这个节点里执行它自己的完整入口代码。子应用内部用什么路由、怎么跳转跟主应用完全解耦。我做过一个数据可视化大屏项目需求是把一个满是 ECharts 图表、自带全屏切换按钮的独立页面嵌入到后台壳里直接用 loadMicroApp 挂在一个全屏区域主应用所有路由规则完全不用动干净利落。2.2 沙箱机制不是有和没有的区别而是你怎么配置它继续说沙箱。这里的差异不是registerMicroApps 有沙箱、loadMicroApp 没有而是两者在沙箱初始化和作用域控制上的配置入口不同。registerMicroApps 在全局注册时通过start({ sandbox: true })开启默认沙箱。它会把每个子应用的 JS 运行环境隔离在一个代理对象里子应用里定义的全局变量、对window的修改在应用卸载后会被清理或还原。这个机制对于多个子应用互不可见这种需求非常关键——不同团队开发的模块每个模块都可能往全局挂了东西不隔离的话早串了。loadMicroApp 的沙箱其实也是默认开启的但它给了你更细的控制维度。最主要的是experimental_dynamicPublicPath这个选项它在手动加载模式里能直接控制子应用是否可以在运行时修改自己的 public path。这个在注册模式下配置起来要绕一些但在手动加载模式下是直接作为loadMicroApp的配置项传入的。我自己的经验如果子应用中用了 CDN 分发资源那就把这个选项打开否则子应用里动态加载的模块很容易按主应用的 public path 去拼资源 URL然后 404。沙箱内部实现上Qiankun 目前有三套沙箱实现针对不支持 Proxy 的老浏览器用快照沙箱现代浏览器默认走 Proxy 沙箱同时对于 preserveGlobalThis 场景还有一套严格隔离的沙箱变体。它们的差异我在第五章的性能对比里会再展开。这里先给一个结论注册模式下如果你开启沙箱全院子应用共享同一套全局策略手动加载模式下每个 loadMicroApp 实例的沙箱是相互独立的可以自由选择对谁关、对谁开。这种灵活度在一些特殊场景里非常有用——比如某个第三方系统就是要在全局存 token你不想为了兼容它把全局沙箱调得乱七八糟。3. 两种模式的加载生命周期init 阶段最容易踩的执行序细节生命周期是这两种模式差异最集中的地方而且很多坑不跑一遍根本意识不到。为了不把读者带进抽象的框架内部我先用一组对比来说清楚。3.1 registerMicroApps 的全局调度与 loadMicroApp 的独立实例注册模式下Qiankun 内部有一个全局的微应用管理器它维护着一个子应用队列按注册顺序、路由规则统一编排。子应用暴露的生命周期函数bootstrap、mount、unmount、update会在调度器触发时被依次调用。这种设计适合一个主应用管理几十个平行子应用的场景因为你没有精力在业务代码里一个一个手写挂载逻辑。手动模式下loadMicroApp每次调用返回的是一个独立的 MicroApp 实例引用它自己走一条完整的生命周期链下载入口 HTML → 解析 JS/CSS → 初始化沙箱 → 执行 bootstrap → 执行 mount。主应用可以在任意时刻调用这个实例的unmount()或update()方法。也就是说你拿到了整套生命周期的遥控器可以随时暂停、恢复、销毁而不需要等路由变化。这里有一个常被忽略的执行序细节注册模式下子应用入口 HTML 的下载时机是路由命中后而手动模式下loadMicroApp调用的一瞬间就会开始下载并且事件监听比如对 popstate、hashchange 的监听是在图片预加载完成后才真正注册到 window 上的。这意味着如果你在调用 loadMicroApp 之后立刻访问某些 window 事件可能拿不到子应用注册的监听器。我亲眼见过一个同事的代码loadMicroApp 之后马上window.dispatchEvent(new HashChangeEvent(...))试图通知子应用内部路由跳转结果子应用根本没收到——因为它的监听器还没挂上。这个时序问题在注册模式下几乎不会出现因为注册模式的路由命中本身就晚于初始化。3.2 生命周期返回值loadMicroApp 解绑与内存泄漏风险再说loadMicroApp返回的实例它有一个unmount()方法是注册模式下没有的显式解绑能力。用法很简单const microApp loadMicroApp({ name: reports, entry: //xxx/reports, container: #report-container, props: { from: dashboard }, }); // 需要卸载时 microApp.unmount();看起来是不是很简单但问题是很多团队在 React 组件里用 loadMicroApp 时忘记在组件卸载的 useEffect cleanup 里调用 unmount导致子应用虽然从 DOM 上被移除了但沙箱环境、事件监听、全局变量并没有被释放。我后来在新项目中定了一条硬性规范凡是用 loadMicroApp 的地方必须在同一个生命周期容器里保证配对挂载即记录实例卸载必调 unmount而且在路由级组件里还要考虑组件被 keep-alive 缓存时的特殊情况。注册模式下你基本上不用操心这些问题框架统一接管、统一清理但这种省心在手动模式下是不存在的。4. 混合使用的真实工程实践什么时候该串什么时候该并很多团队问过我同一个项目里能同时用 registerMicroApps 和 loadMicroApp 吗答案是可以而且实际项目中很常见。我自己维护过的一个工作台项目就是这种形态左侧主框架用 registerMicroApps 注册了订单、商品、客户三个核心子应用按路由自动切换右侧呢一个 AI 选品助手 的独立工具页是用 loadMicroApp 手动挂上去的因为它只出现在一个特定的营销活动面板里不在任何常规路由中。两套模式并行了大半年基本没有冲突。但混用有几个前提我们先说清楚否则很容易翻车混用时loadMicroApp 挂载的子应用不建议再出现在 registerMicroApps 的注册表里。否则同一个 name 在两种模式调度下会出现实例冲突表现就是页面刷新后子应用被挂载两次或者卸载时报应用不存在。如果你打算在同一个页面先用 loadMicroApp 挂一个子应用过一会儿又要用 registerMicroApps 的规则把它激活一遍那我建议你别这么设计。这不是技术做不到而是心智负担太重。我做过一次类似的改造最后把业务需求调整成手动模式优先注册表只保留全局应用调度逻辑一下子干净了。动态基座概念当你用 loadMicroApp 挂载时如果子应用里又调用了loadMicroApp加载另一个子应用就形成了嵌套微前端。Qiankun 对这种嵌套是支持的但你必须保证内层和外层的沙箱状态一致否则内层子应用拿到的window对象和外层不是同一个代理各种隐式全局变量传递都会出问题。下面是我整理的一页混用清单可以在项目启动前给团队评审用场景推荐模式理由中后台整体集成、按菜单切换registerMicroApps路由自动调度团队接入成本低独立页面嵌入报表、编辑器、大屏loadMicroApp路由解耦容器挂载自由动态从后端拉取应用配置再挂载loadMicroApp注册表是静态的手动模式可动态扩展多实例同屏展示AB 对比loadMicroApp 多次调用注册模式天然单实例无法同屏多个不同应用主应用域名下多个团队独立部署registerMicroApps统一接口管理构建产物和依赖关系5. 性能与首屏优化预加载和缓存策略的取舍聊完工程形态接下来是很多中大型团队最关心的性能问题。两种模式在预加载策略上差别巨大。5.1 prefetchAfterCreation vs prefetch 属性别被文档里的两句话糊弄registerMicroApps 注册子应用时可以在start({ prefetch: all })或start({ prefetch: [/orders, /products] })里配置预加载规则。默认行为是第一个子应用激活后其余子应用的入口 HTML 会在空闲时间被拉取并执行 JS 解析但不触发真正的 mount。这种预加载可以显著减少路由切换时的白屏等待代价是额外的网络请求与解析开销。Qiankun 内部对此还做了一个很贴心的小处理会根据网络类型评估——如果你在弱网环境预加载会调整为更保守的策略并不会把所有子应用全部拉一遍。loadMicroApp 的预加载属性更直接它默认会预加载指定的 app 的 HTML。你可以在配置里设prefetch: all来一次性预加载所有手动挂载的子应用。但请注意手动模式下预加载的触发节点是调用 loadMicroApp 脚本执行后空闲时而不是首个子应用激活后因为没有任何路由规则来告诉你哪个才是先手。如果你在页面初始化时连续调用多个 loadMicroApp会观察到浏览器一次性发起了大量网络请求。所以我通常建议手动加载模式下少用全量 prefetch优先按业务路径去预加载那些用户很可能会点开的应用并且用import-html-entry的缓存机制避免重复请求。预加载这件事注册模式更像图书馆提前把书翻到某一页手动模式则是你决定要不要把整本书都放在桌上。5.2 沙箱性能开销的真实数据proxy 完胜但不是免费的再给一组沙箱性能对照。Proxy 沙箱是 Qiankun 默认对现代浏览器使用的方式它通过代理 window 对象来拦截读写操作性能远好于旧版快照沙箱。快照沙箱在每次 mount/unmount 时要遍历 window 的所有 key 做快照和恢复如果你的页面挂在大量第三方 SDK 插件这个遍历时间可能达到几十毫秒甚至更多。我实测过一个挂满地图 SDK 的后台页面切换子应用时仅沙箱自带动作耗时对比沙箱类型应用内切换耗时全局变量污染风险适用浏览器快照沙箱用户可感知的卡顿数次低但恢复粒度粗糙老版本浏览器Proxy 沙箱几乎无感低且拦截粒度细现代浏览器严格隔离沙箱preserveGlobalThis略有增长极低隔离最彻底对安全敏感的金融/政务场景由于 loadMicroApp 支持直接针对单个子应用设置sandbox: { loose: true }或者strict模式性能调优的人通常会在手动模式下对重子应用单独做沙箱加固而在注册模式下则只能全局调优。区别就在这里registerMicroApps 的沙箱是全局开关loadMicroApp 的沙箱是单实例旋钮。这决定了你在性能优化和隔离安全之间的取舍空间。6. 一次生产事故的完整排查链路loadMicroApp 挂载的子应用 hash 路由回退失效接下来我想分享一个我真实遇到过的生产事故。它不常见但一旦出现排查起来相当能体现两种模式差异的威力。就是一个用 loadMicroApp 手动挂载的子应用在父应用里点击其内部路由时偶尔会出现父应用路由也发生变化的现象甚至导致父应用页面被带回到之前的菜单项。6.1 现象与第一轮排查从回退失效到 路由抢占当时的架构是父应用使用 hash 路由子应用也用 hash 路由子应用通过 loadMicroApp 挂在一个独立区域。某天测试反馈在子应用里连续点击几个内部菜单跳转后再点击返回按钮页面不是回到子应用的上一个内部页面而是直接跳出了子应用把父应用路由也重组了。更奇怪的是这个过程不是每次都发生刷新后能复现但换个操作路径又正常。第一轮排查我们怀疑是子应用内部路由没有使用相对路径导致 hash 变更时父应用也监听到并去做路由匹配。但检查之后发现子应用的路由跳转都是/report/detail这类带前缀的 hash理论上不会影响父应用。我们接着打开了 Qiankun 官方提供的 getMicroAppStateActions 相关调试状态查看发现父应用认为当前激活的仍是子应用但地址栏已经被改写了。6.2 根因定位loadMicroApp 返回实例的 unmount 时机第二轮深挖时我们才想起来loadMicroApp 返回的实例虽然有unmount()方法但我们在父应用某个全局菜单切换函数里使用所有子应用在切换前统一解绑的逻辑。问题恰恰出在这个统一解绑上如果父应用在子应用内部路由跳转过程中调用了 unmount而子应用内部的路由监听机制还在执行那么 unmount 会把沙箱重置、事件监听移除但子应用最后一个 hash 变更事件已经冒泡到了父应用的路由层上。父应用以为自己返回到了之前的菜单项实际上是被子应用路由变化给误触发了路由切换。进一步验证我们在加载 loadMicroApp 之后给容器内的子应用事件做了一次隔离确认 popstate 和 hashchange 的监听对象作用域。测试发现注册模式下由于子应用的事件监听器是注册在沙箱 window 上父应用完全感知不到子应用路由变化而手动模式下如果沙箱消费了事件且未阻止传播事件会被窗口对象接收父应用路由监听也就可能捡漏。6.3 修复方案与复盘收获修复方案其实不复杂在手动挂载子应用时给它的内部路由监听事件的传播范围做收窄同时在父应用路由切换前不再暴力解绑所有 loadMicroApp 实例而是等实例自身确认 unmount 完成后再切换父应用路由。另外一个细节是我们在 loadMicroApp 的配置里手动指定了sandbox: { experimentalDynamicPublicPath: true }避免子应用内部资源加载误触发父应用脚本重编译进一步缩小了事件冲突范围。复盘中我最大的感受是这两种模式在路由关联和事件传播上的差异比表面 API 看起来的差距要深层得多。注册模式的沙箱调度像一个保安把整个子应用隔离在一个房间内外人看不到里面的灯光手动加载模式则更像租了一个共享办公区隔断是有了但门没锁进出的人可能把公共走廊的灯也带亮。这也是为什么我在团队规范里写明只要不是必须手动控制的场景优先用 registerMicroApps。loadMicroApp 是很好的解药但不是所有病都该用它来治。7. 到底怎么选一张决策表与给团队的实操建议把上面所有分析收敛成一句话能用自动就别手动需要自由才谈手动。但我明白很多团队是到了项目中途才需要在两种模式之间切换所以我再给一张更务实的选型决策表以及几条实实在在的工程建议。信号选择 registerMicroApps选择 loadMicroApp子应用数量大于 3且模块边界清晰1~2 个工具型应用路由方式主应用和子应用统一为 hash/web history子应用有独立路由体系、需要隔离处理业务集成方式跟随主应用菜单按需加载固定页面上嵌入无全局路由联动性能需求首屏要快预加载策略由框架统一处理只需在特定操作触发时再加载不着急预取沙箱定制可全局开关统一流程简洁需要对单个应用单独调整沙箱参数团队维护能力有专人负责微前端架构希望尽量少侵入主应用代码给团队的实操建议初始架构未定默认从 registerMicroApps 起步。不是因为它是默认选项而是因为它的约束力反而能帮你避免很多微前端的隐性复杂度。等真的遇到路由绑架了再评估要不要把某一个页面改到 loadMicroApp。这样渐进式改造比一开始全盘手动要可控得多。如果要混用务必单独维护两个注册表。把 registerMicroApps 管理的应用和 loadMicroApp 管理的应用分别列出来明确哪些应用只出现在自动路由里、哪些只出现在手动挂载里。同时混用时注意同一个子应用的入口 document 加载缓存Qiankun 默认缓存每个 name 的入口信息如果手动和自动两种路径都加载过一次状态可能互相影响。全链路加监控别依赖控制台心态。微前端架构引入后我强烈建议在子应用加载失败、沙箱初始化异常、global 变量重名等关键节点做上报。注册模式的好处是这些节点都在框架内部统一处理后走 ErrorHandler手动模式下错误可能直接穿透到业务代码。你至少需要一个errorHandler来统一收集异常我在主应用启动时就加上——import { loadMicroApp, registerMicroApps, start } from qiankun; // 自动模式 registerMicroApps([ { name: orders, entry: //localhost:3001, container: #frame-container, activeRule: /orders, }, ]); start({ sandbox: { strict: true } }); // 手动模式注意基础配置项 const reportsApp loadMicroApp( { name: reports, entry: //localhost:3002, container: #report-container, props: { from: main }, }, { sandbox: { strict: true }, errorHandler: (error) { console.error([qiankun] 手动加载 reports 失败, error); }, } ); // 外部可调用 reportsApp.unmount();接手别人项目时先查 loadMicroApp 的调用方是否都有对应 unmount。这是我排查过好几次内存泄漏问题后总结出的规律。手写一个快速检查脚本列出所有loadMicroApp(调用逐个确认返回实例有没有被保存并用于 unmount。发现没保存的代码几乎都可以判定为潜在 bug。用 Qiankun 做了三年多微前端我的体会是两种加载模式不是优劣之分而是复杂度阈值之分。注册模式牺牲自由度换来了稳定和统一适合一个团队统一治理手动模式用自由换来了灵活和动态能力适合快速嵌入独立模块。判断一个架构决策是否合理不要只看 API 功能多少更要看你的团队是否有足够的能力去驾驭这种自由。真正让我觉得 Qiankun 设计得不错 的时刻不是它提供了多牛的 API而是它在文档里反复强调你可以用 loadMicroApp但请确认你真的需要它——这种克制才是工程框架最难得的品质。