微前端这套架构,圈内聊起来都是"主应用 + 子应用 + 路由分发 + 沙箱隔离"一套组合拳,听起来特别规整。但真正接手大型项目、把两三个团队的业务模块塞进同一个页面之后,你会发现最先出问题的往往不是路由,也不是状态管理,而是看似最基础的DOM 操作与事件处理。
我之前在带一个 vue2 老项目做 qiankun 微前端改造时,就遇到过一个特别诡异的场景:子应用里弹出一个 Modal,主应用的页面竟然跟着锁滚动;A 子应用注册了一个全局快捷键,切到 B 子应用之后快捷键依然生效,而且更离谱的是把 B 里同一个键位的功能给覆盖了。翻遍文档、查遍沙箱源码,最后发现这些问题的根源全部指向同一个地方——我们对"DOM 边界"和"事件流"的理解还停留在单页应用的惯性里。
这篇文章不聊微前端的概念科普,也不贴 qiankun 的 API 文档,只讲清楚一件事:在微前端架构下,DOM 操作和事件处理到底该怎么设计、怎么排查、怎么避坑。内容会结合 vue2 + qiankun 的实际场景展开,但大部分方法论对 any 框架的微前端落地都适用。
1. 微前端落地后,最先崩的往往是事件流
很多人理解微前端,会把它想成"把几个独立的网页拼在一起",每个子应用各自运行、互不干扰。这个理解方向没错,但粒度太粗了。真实情况是:所有子应用共享同一个 document、同一个 window、同一棵真实的 DOM 树。只不过每个子应用的代码被包裹在各自的沙箱容器里,看起来像隔离了而已。
1.1 微前端不是"多页面拼接",而是多个运行时共享同一个 document
先厘清一个关键认知。在传统多页应用里,一个页面只有一个前端运行时,DOM 树完全归它管。在纯单页应用里,整个 document 也是独占的。但在微前端架构下,主应用和多个子应用是同时存活在同一个页面上下文中的多个运行时。
qiankun 这类框架做的事,本质上是通过 HTML Entry 加载子应用的 JS 和 CSS,然后在一个约定的容器节点(比如#app-child-a)里渲染子应用。子应用仍然是一套标准的 SPA 逻辑,有自己挂载点、自己的路由、自己的组件树。但要注意,这个挂载点只是 DOM 树上的一个子节点,浏览器的事件派发机制不会因为你用了微前端框架就网开一面——事件从目标节点产生后,会一路冒泡到 document,经过沿途每一个祖先节点,这中间包括主应用的容器、主应用的根节点,以及 document 本身。
所以会出现一个非常反直觉的现象:你在子应用里点了一个按钮,子应用自己的监听器当然会触发,但主应用在 document 上注册的全局监听器同样会收到这个事件。反过来也一样,主应用某个区域触发的键盘事件,如果子应用在 window 上做了监听,同样能收到。这就是微前端事件问题的总根源。
1.2 JS 沙箱管得住变量,管不住浏览器原生的事件派发
qiankun 提供了三种 JS 沙箱策略:快照沙箱、代理沙箱、严格沙箱。它们的作用范围是 JS 层面的全局变量、window上的属性读写。也就是说,子应用里window.a = 1不会污染主应用,子应用之间的全局变量也互相看不见。但问题是——浏览器的事件机制根本不在 JS 沙箱的管辖范围内。
事件派发是由浏览器内核基于真实 DOM 树结构完成的。子应用里按钮的 click 事件,天然就会冒泡到主应用的根节点和 document。这不是谁能通过"配置一下"就能关掉的特性,它就是 Web 平台最底层的机制。沙箱管的是变量,管不了事件流。
这带来的直接后果就是:开发环境下子应用独立运行得好好的,一合进主应用,各种全局事件监听互相干扰的问题就全冒出来了。因为独立运行时,子应用自己就是 document 的主人,监听 window 和 document 天经地义;合进微前端后,你再监听 document,就是在一群共享这个 document 的房客里大声说话——所有人都听得见。
1.3 放下"独立应用"的幻觉,建立"DOM 子树"心智模型
踩过一轮坑之后,我最大的收获是必须更新对子应用的定位认知。在微前端架构下,每个子应用都不再是"一个应用",而是一棵挂在主文档某棵子树下的组件集合。这句话听起来简单,但真能落到代码里很难。
举几个具体表现:
- 子应用的根容器不是页面,只是
document.body下某个节点。你要避免在代码里直接操作body、html这类全局节点,比如往 body 上挂弹窗、给 body 加 class 控制滚动。 - 子应用内的选择器如果是写在全局 CSS 里的,几乎必然会穿透到主应用和其他子应用。这也是为什么微前端里样式隔离只能靠框架提供的
scoped或者shadow DOM兜底。 - 事件监听的默认绑定目标应该是"子树根节点"而不是
window或document,除非这个监听在设计上就是要跨应用的。
把这个心智模型建立起来之后,后面所有事件相关的设计和排查都会顺很多。
2. 三个真实事故:从现场现象到根因定位的完整链路
纸上谈兵没意思,我直接复盘三个自己在微前端改造中真实遇到的事件冲突事故。每个事故都按"现象 → 排查过程 → 根因 → 修复"的顺序拆开讲,你可以直接对照自己的项目去寻找同类问题。
2.1 事故一:子应用弹窗弹出来,主应用页面跟着锁滚动
现象:子应用里有一个表单页,点击按钮弹出一个 Modal(基于 element-ui 的 Dialog)。在独立运行时一切正常,Modal 打开后背景页面锁定滚动,关闭后恢复。合入微前端后,Modal 一打开,主应用的整体页面也跟着锁滚动了;而且 Modal 关闭之后,主应用的滚动条恢复不了,需要手动刷新页面才恢复正常。
排查过程:一开始以为是 element-ui 弹窗的样式在微前端下没加载对,检查了样式和 z-index 都没问题。后来用 Chrome DevTools 的事件监听断点功能,在 click 事件上打了断点,逐步查看 Modal 打开的过程,发现 element-ui 的 Dialog 组件在打开时会向document.body上添加一个监听滚轮事件的 handler,用来在弹窗内部滚动时阻止背景滚动。
问题就出在这——当子应用独立运行时,document.body就是子应用自己的 body,阻塞滚动和移除监听的逻辑完全一致;但合入微前端后,document.body变成了主应用的 body,子应用弹窗的阻塞逻辑就直接作用到了主应用全局。尤其是"关闭时移除监听"这一步,如果组件销毁时机不对(比如弹窗还没销毁组件就被微前端切走了),监听器就永远留在 body 上,滚动锁就再也解不开了。
根因:不是 qiankun 的 bug,也不是 element-ui 的 bug,而是子应用把 DOM 操作目标指向了全局节点(body),在微前端共享 document 的语境下,这个"全局"被放大成了整个主应用的全局。
修复:给子应用的弹窗组件设置modal-append-to-body=false,让弹层层挂载到子应用自己的容器节点内。同时约定:子应用内所有需要挂载"浮层"的场景(下拉框、日期选择器、弹窗、通知),都优先检查挂载点,默认挂到子树根节点内,而不是盲目接受第三方组件库的默认body挂载行为。
2.2 事故二:A 子应用注册的快捷键,把 B 子应用的覆盖了
现象:主应用内同时注册了 A、B 两个子应用。A 子应用在代码里监听window.addEventListener('keydown', handlerA),支持按Ctrl + S保存当前页面。B 子应用也监听keydown,用Ctrl + S做别的操作。结果用户在 B 子应用里按快捷键时,触发的是 A 的 handler——B 的功能完全没反应。进一步测试发现,后加载的子应用注册的监听器,会把先加载的覆盖掉,因为同一个 keydown 事件在 window 上的监听器是"按注册顺序全部执行"的,而且一旦前面一个 handler 里执行了stopImmediatePropagation(),后面所有 handler 都不再执行。
排查过程:最开始怀疑是沙箱导致的事件丢失,于是把两个子应用的 handler 都打印出来对比。用getEventListeners(window)在控制台查看,发现 window 上同时挂了一堆来自不同子应用的 keydown 监听器,分别指向不同的函数实例。这证实了事件并没有丢失,而是全部堆积在 window 上,触发的顺序和时机完全不受子应用边界控制。
根因:全局事件监听被当成"页面私有资源"来用,但在微前端里 window 是公共资源。子应用之间对同名事件的同名监听,天然会发生冲突;更危险的是,如果一个老团队习惯在 handler 里不判断事件来源就执行全局逻辑,另一个子应用的键盘操作可能被"静默吞掉"。
修复:分两步。第一步,A 和 B 子应用的快捷键监听都改成"只在自己的子树根节点上监听"或者"监听时先校验当前激活的应用是否是本应用"。第二步,如果确实需要全局快捷键(比如全局搜索),由主应用统一注册一个快捷键管理中心,子应用通过接口声明自己的快捷键,由主应用按当前激活路由分发。这样既保留了快捷键能力,又消除了多点注册的混乱。
2.3 事故三:主应用的全局点击委托,把子应用的表单提交拦截了
现象:主应用为了统计用户行为日志,在document上挂了一个全局 click 委托,凡是点击[data-track-id]属性的元素就上报数据。结果上线后,子应用反馈:表单点击提交按钮时,部分表单校验逻辑失效了;还有的用户连续点击提交按钮,生成了多条重复数据。
排查过程:一开始怀疑是子应用的表单校验代码有 bug,但子应用独立运行就完全正常。后来在主应用的全局 click 委托里加日志,发现每次子应用的按钮被点击后,主应用委托在冒泡阶段捕获到事件,并且对目标元素做了若干处理——其中有一段逻辑会调用element.closest('form')去判断点击是否在某个表单内,如果判断命中,就执行preventDefault()阻止默认行为,用来避免某些情况下误触发表单提交。
问题就出在这里:这个"某些情况"的判断条件写得太宽松,它根本不知道事件目标元素其实属于另一个子应用。子应用里点击提交按钮,事件冒泡到 document,主应用的委托逻辑用closest('form')找到了子应用里的 form 元素,然后当成"应该拦掉的提交"给拦了。
根因:document级事件委托天然跨越所有 DOM 边界。委托逻辑里对元素的查找和判断(比如closest会检索整个祖先链)完全无视子应用之间的隔离。这个本质上是主应用"越界执法"。
修复:给委托逻辑加一道"边界检查"——只有在事件目标位于主应用管辖区域内时,才执行主应用的委托逻辑。具体做法是维护一组合法的容器节点,判断container.contains(event.target)为真时才继续处理。同时在子应用内部,如果需要拦截事件,应该在子应用的子树根节点上做委托,而不是依赖主应用的全局委托来睁一只眼闭一只眼。
2.4 定位事件问题的通用排查方法
经历这几个事故后,我总结了一套通用排查流程,现在遇到任何"微前端下事件行为异常",基本都是按这个顺序定位:
- 最小化复现环境:先把其他子应用全部禁用,只保留一个子应用,跟主应用联调。如果问题消失,那就是子应用之间或主应用与子应用之间的相互干扰;如果问题还在,再逐步叠加条件。
- 打印事件完整链路:在事件监听回调里输出
event.path(或event.composedPath,兼容性更好),可以看到事件从目标节点一路冒泡到 document 的完整路径。对照这个路径,就能明确它经过了你预期之外的哪些节点。 - 确认哪一层监听了:利用 DevTools 的
getEventListeners(node)命令,列出某个节点上绑定的所有监听器。这一步能快速看出目标节点、body、document、window 上分别有哪些监听器,来自哪个子应用。 - 按三层逻辑定位根因:哪一层监听的 → 谁先注册的 → 是否执行了
stopPropagation或stopImmediatePropagation。三层逐一确认,基本能把问题圈定到一个具体的 handler 上。
这套方法本质上不依赖任何微前端框架,纯靠浏览器的标准调试手段就能实现。
3. 事件隔离的实战打法:注册、转发、销毁三件事
理解了问题根源之后,真正要落地的是设计方案。我们项目最终定下来的事件隔离规范可以浓缩成三件事:注册有边界、转发要有协议、销毁必须彻底。下面展开讲。
3.1 默认监听子树,不碰 window 和 document(除非万不得已)
这是整个事件隔离方案里最基本、也最容易被忽略的一条。所有子应用内部的业务事件,监听器一律挂在子应用自己的容器根节点上,而不是 window、document 或 body 上。
举个例子,vue2 项目里常见的写法是:
// 反例:全局监听,污染整个微前端环境 mounted() { window.addEventListener('keydown', this.handleKeydown) }, beforeDestroy() { window.removeEventListener('keydown', this.handleKeydown) }在独立运行时,这段代码没有任何问题。但放到微前端架构下,window是共享的,上面挂满各种 handler 是必然的事故温床。更稳妥的做法是:
// 正例:在子树根节点上监听 mounted() { this.$el.addEventListener('keydown', this.handleKeydown) }, beforeDestroy() { this.$el.removeEventListener('keydown', this.handleKeydown) }如果你用的是 vue 的@keydown指令,那默认就是绑定在当前组件模板的根节点上的,不涉及全局污染。但如果你的场景真的需要全局监听——比如快捷键全局使用、或者在某个区域内监听拖拽事件时,必须走到 window 上——那请往下看第 3.4 节的"激活态闸门"做法。
3.2 命名空间与监听器登记簿
即使把所有监听都限制在子树内,子应用内部仍然可能有多个页面、多个组件同时注册事件。为了后续能精确销毁、避免重复注册,我们约定每个子应用内部事件都带上前缀,格式是appName:eventName。
比如:
this.$el.addEventListener('app-a:form-reset', this.handleFormReset)这样做的直接好处是:当你在 DevTools 里用getEventListeners查看某个节点上的监听器时,一眼就能分辨哪个事件属于哪个子应用。在跨团队协作时,这个前缀也成了一个明确的"所有权声明"——看到app-a:开头的事件,就知道这是 A 团队负责的逻辑,出了问题找对应团队,沟通成本大幅下降。
同时,我建议给每个子应用维护一个"监听器登记簿"。就是一个简单的 Map 结构,key 是事件类型,value 是 handler 引用数组。这样在卸载时可以统一移除,不用一个个追着去查代码里注册过哪些监听。
// 简版登记簿 const listenerRegistry = new Map() export function registerListener(target, event, handler, options) { target.addEventListener(event, handler, options) const key = `${event}:${handler.name || 'anonymous'}` if (!listenerRegistry.has(key)) { listenerRegistry.set(key, []) } listenerRegistry.get(key).push({ target, handler, options }) } export function unregisterAll() { listenerRegistry.forEach((items) => { items.forEach(({ target, handler, options }) => { target.removeEventListener(handler, options) }) }) listenerRegistry.clear() }这个工具类不复杂,几十行代码,但在微前端里能救很多命。
3.3 在 mount/unmount 钩子里强制做事件生命周期管理
微前端框架(包括 qiankun)给子应用暴露了bootstrap、mount、unmount三个生命周期钩子。我们约定:所有子应用的全局事件资源,必须在mount时注册,在unmount时释放。
这里有个很容易踩的坑:vue2 项目在beforeDestroy里清理事件,但如果用户从子应用切换到主应用(而不是关闭子应用页面),vue 组件的beforeDestroy不一定会执行。qiankun 在切换子应用时,调用的不是 vue 组件的销毁钩子,而是子应用自己的unmount钩子。所以如果你的清理逻辑只放在beforeDestroy里,等于是没清理。
正确的做法是:
// qiankun 生命周期内管理事件 export async function mount(props) { render(props) // 注册事件 window.$eventBus && window.$eventBus.on('app-a:refresh', handleRefresh) } export async function unmount() { // 统一销毁 window.$eventBus && window.$eventBus.off('app-a:refresh', handleRefresh) unregisterAll() // 再清理 vue 实例 instance && instance.$destroy() }另外,如果你用了 vue-router 且开启了 keep-alive,监听器的清理时机更复杂,建议把"监听器登记簿"的清理统一放在子应用的unmount钩子里,而不是依赖组件的destroyed钩子。这是我们在实际项目中多次踩坑后最终定下的规范。
3.4 需要全局监听时,加"激活态"闸门
有些事件确实绕不开全局监听。比如全局快捷键、或者拖拽跨应用边界时的mousemove。这种场景不能因噎废食不去用 window/document,而是要加一道"激活态"判断。
具体思路是:在 window 上注册事件监听时,回调函数的第一行先判断"当前激活的应用或路由是不是自己",如果是才继续执行,如果不是直接 return。
// 主应用中维护的当前激活应用信息 window.__ACTIVE_APP__ = 'app-a' // 子应用 A 的全局监听 window.addEventListener('keydown', (e) => { if (window.__ACTIVE_APP__ !== 'app-a') return // 处理 Ctrl+S })激活态的维护可以由主应用在路由切换时写入,也可以由微前端框架的props传递。关键点是让每个监听器都具备"自检"能力,而不是假设所有全局事件都是给自己的。
这里要特别说明:激活态闸门是"兜底策略",不是"首选策略"。所有能绑定到子树根节点的监听,都应该优先走子树监听方案;只有实在无法摆脱全局的极少数场景,才用这个方案。如果一上来就全局监听 + 激活态判断,等于把每个子应用的事件逻辑全部暴露在公共命名空间下,随着子应用数量增长,管理成本会指数级上升。
4. 跨应用通信的事件设计:从广播风暴到契约通信
微前端架构下,很多团队会选择用事件机制做跨应用通信——在主应用里window.dispatchEvent(new CustomEvent('xxx', { detail: data })),在子应用里window.addEventListener('xxx', handler)。这个方案上手快,但用一阵子就会发现它暗藏大坑。
4.1 直接派发自定义事件的问题
直接往window上派发自定义事件,代码写起来很爽,但后患无穷:
一是来源不透明。任何一个子应用都能派发同名事件,收到事件的另一方根本不知道事件来自哪里,排查问题时无从下手。
二是类型不约束。CustomEvent的detail是任意对象,发的人传{ userId: 123 },收的人可能读的是{ user_id: 123 },字段不一致的问题在跨团队协作中几乎一定会发生,而且往往要等线上故障才能发现。
三是隐式耦合。全局事件本质上是一种"总线黑盒"。A 应用派发事件,并不知道谁在监听、有几个监听、监听方会不会影响自己的业务。这种隐式关联在大型项目里是最难维护的隐性债务。
四是没有"一次订阅、按需卸载"的语法糖。原生addEventListener+removeEventListener需要开发者手动保证配对,而实际开发中忘移除的案例比比皆是。
4.2 用几十行代码实现一个受控事件总线
与其用裸的window事件满天飞,不如在子应用内部实现一个受控的事件总线。核心逻辑就几点:事件类型白名单、来源标记、订阅退订的 API 封装。
这里给一个精简实现(几十行):
// eventBus.js — 受控事件总线 const handlers = {} const sourceApp = window.__APP_NAME__ || 'unknown' export function on(eventType, handler) { if (!handlers[eventType]) { handlers[eventType] = [] } handlers[eventType].push(handler) return () => off(eventType, handler) } export function off(eventType, handler) { if (!handlers[eventType]) return handlers[eventType] = handlers[eventType].filter((h) => h !== handler) } export function emit(eventType, payload = {}) { if (!handlers[eventType]) return const eventData = { source: sourceApp, payload, timestamp: Date.now(), } handlers[eventType].forEach((handler) => { try { handler(eventData) } catch (e) { console.error(`[eventBus] handler error on ${eventType}:`, e) } }) }注意几个关键设计:
source字段由当前子应用名自动标记,所有订阅方都能看到事件来源。emit内部对每个 handler 的异常做了 catch,避免一个 handler 抛错中断整条事件链。on返回一个退订函数,方便在组件卸载时直接调用,不用反复保存 handler 引用。
这个总线的价值不在于它比原生事件强大,而在于它把事件通信的行为收敛到了一个可控的模块里。你需要排查事件问题时,只要在这个模块的emit和on里打日志,就能看到全部通信链路。
4.3 事件与状态要分开:通知用事件,数据用 store
在跨应用通信的设计中,一个很重要的原则是把"事件"和"状态"分开对待。
事件适合表达"发生了某件事",比如用户登录了、订单创建了、页面切换了。它是一次性的、偏动作语义的。
状态适合表达"当前是什么情况",比如用户的昵称、购物车的商品列表、当前的权限集合。它是持续性的、偏数据语义的。
很多团队喜欢把所有跨应用数据交互都做成事件,A 应用派发一个"更新购物车数量"的事件,B 应用监听到后去改自己的本地状态。短看没问题,但当事件数量变多、链路变长之后,你很难记住某个状态到底是哪个事件、从哪条链路更新过来的。
更好的做法是:状态交互走共享 store,通过 store 的 getter 读取、通过 action 更新;事件只用于通知"状态已变化",不直接携带大块数据。比如购物车数据放共享 store,购物车数量变化后,由 store 的订阅机制自动通知所有关注方,而不是靠emit('cartChanged', { items: [...] })把整个购物车数据广播一遍。
这样做的好处非常明显:一是数据流向清晰,你可以随时查看 store 里某个字段当前的值是什么,而不需要回溯事件链;二是状态的一致性由 store 统一保证,不会出现 A 事件的 payload 和 B 事件的 payload 相互矛盾的问题;三是后续做持久化、调试、故障恢复都方便得多。
4.4 主应用做事件网关的取舍
在跨应用通信的架构模式上,行业内大致有两个方向:点对点直连和主应用网关。
点对点直连就是子应用之间通过事件总线直接通信,优点是实现简单、延迟低,适合小规模、少耦合的场景。缺点是通信链路不可控,一个事件发出来,你无法在全局层面看到它经过了哪些应用、产生了什么副作用。
主应用网关模式,是子应用之间不直接通信,所有事件统一发送到主应用,主应用根据事件类型和目标应用完成路由转发。这种模式有几大优势:
- 可观测性强:所有通信都经过主应用的"事件路由",你在主应用里加一条日志就能看到全链路。
- 便于做权限控制:主应用可以过滤掉某些来源、某些类型的事件,防止误操作或者越权调用。
- 版本兼容性好:当子应用升级后,事件协议变更不需要所有应用同步改,主应用可以在网关层做兼容转换。
缺点是主应用会成为一个通信瓶颈,如果事件量很大,主应用需要承担不小的转发压力;同时主应用的代码会膨胀,需要维护一套事件路由表。
我们项目的取舍是:小团队、事件量可控时直接点对点;一旦超过 5 个子应用、或者涉及跨团队协作,强制走主应用网关。这套规则不复杂,但对秩序感的要求很高,最好在项目一开始就定好,而不是等踩了坑再补。
5. 进阶场景:弹窗挂载、拖拽跨界与监听器泄漏
基础的事件隔离方案落地后,还有几个进阶场景需要额外注意。这些场景不会每天遇到,但一遇到就是大坑。
5.1 弹窗挂到 body 下引发的事件与样式连锁反应
前面 2.1 里提到过一个弹窗锁滚动的事故,这里展开讲的更远一些。UI 组件库(element-ui、ant-design 等)的弹窗、下拉框、日期选择器,默认大多挂载到document.body下,这是为了保证浮层能覆盖在整个页面最上方。但在微前端架构下,挂到 body 会带来几个连锁问题:
- 样式隔离失效:子应用的 scoped 样式在编译后会给选择器加上
>// 主应用事件日志开关 if (window.__EVENT_DEBUG__) { document.addEventListener('click', (e) => { console.debug('[EVENT]', e.type, e.target, e.composedPath()) }, true) }这个"监听器"不干任何实事,只打印事件链路信息。表面上看起来多了一次事件监听,但它的价值非常大——当线上用户反馈"点这个按钮没反应"时,你可以让用户(或者客服)通过某种安全可控的方式打开日志开关,把控制台截图发回来,你一眼就能看出点击事件冒泡到了哪些节点、经过了多少层、有没有被某个节点的 handler 拦截掉。这种线上排查能力,在微前端架构下尤为珍贵。
注意:这个日志开关在生产环境必须默认关闭,并且日志中不要输出任何敏感信息(比如用户输入内容、token、手机号等)。调试完毕记得关闭开关。安全合规问题在大型项目里永远要放在第一位。
写在最后的经验沉淀
带团队把所有事件边界规范落地,前后花了大半年时间。回头看,微前端的事件治理本质上不是一个技术问题,而是一个工程纪律问题。技术方案再完美,如果团队没有建立起"监听有边界、资源必回收"的习惯,照样会在上线后爆出一堆偶发性事件冲突。
我们最后沉淀下来的几条硬性规则,现在作为新人入职培训的一部分:
- 子应用代码里禁止直接操作
body、html、document节点,所有 DOM 操作必须约束在自身容器内。 - 所有手动添加的事件监听器必须通过 EventManager 注册,并在
unmount中统一销毁。 - 所有跨应用通信必须经过受控事件总线或共享 store,禁止直接在 window 上派发自定义事件。
- 所有弹窗类浮层默认挂载到子树根节点,挂到 body 必须经过架构组 review。
- 全局快捷键需求必须提给主应用统一管理,子应用内不做全局监听。
这几条规则看起来简单,但真正执行到位之后,微前端带来的"事件灾难"基本就绝迹了。如果你正准备做微前端改造,建议把这些规则内化到团队规范里,而不是等事故发生了再想办法。
最后再分享一个开发时的小技巧:在子应用开发环境里,给 EventManager 加一个定时统计任务,每 30 秒打印一次当前注册的监听器数量和分布。这个数字平时不会有人看,但一旦出现诡异的偶发性 bug,它往往能在第一时间帮你定位到"是不是有监听器泄漏了"。用最小的成本,换来最及时的预警,这笔账怎么算都划算。
- 子应用代码里禁止直接操作