ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

微前端通信方案详解:qiankun中应用间通信的实现与选型

微前端通信方案详解:qiankun中应用间通信的实现与选型 1. 为什么应用间通信是微前端架构里绕不开的坎先聊一个我实际遇到过很多次的场景你负责的主应用已经跑得好好的公司决定要上微前端把电商后台拆成用户管理、订单管理、商品管理三个子应用。看起来只是把代码按业务拆开可真到联调那天你会发现用户登录成功后主应用的头部要显示用户名子应用里也要拿到这份用户信息去做权限控制订单子应用里创建了一笔订单商品子应用立刻要知道库存变了要刷新列表。这些事情全都要靠应用之间传数据、发通知来解决。qiankun 作为目前前端圈用得最广的微前端框架核心思路是把多个独立的前端应用组合到同一个页面里。每个子应用都是独立的工程有自己独立的 JavaScript 运行时、独立的依赖包、独立的状态管理它们之间默认是完全隔离的。qiankun 的沙箱机制把 window 上都做了代理和隔离子应用里改一个全局变量根本不会影响其他应用。这种隔离是好事帮你防了很多相互污染的坑但同时也意味着子应用之间想像以前在同一个项目里那样共享一个全局 store、写一个全局事件都行不通了。所以这不是一个要不要做通信的问题而是一个什么时候要通信、用什么方式通信的问题。通信这件事在微前端架构里是刚需只要你的微前端方案不止一个空壳子页面只要子应用之间还有任何业务逻辑上的关联你就必须有一套可靠的通信方案。在我踩过不少坑之后总结下来qiankun 体系里应用间通信的实现方式大致有四类官方自带的 props 传参和 GlobalState、自研的事件总线、基于浏览器存储的自定义事件方案、以及路由参数传值。这几种方案没有绝对的好坏只有适合不适合。下面我一个个拆开来讲每种都会给出代码和适用场景也把容易踩的坑一并说了。2. 官方自带的通信手段先搞清楚它们能干什么2.1 生命周期里的 props比你想的更有用qiankun 官方文档里第一个提到的通信方式就是主应用在注册微应用的时候通过 props 向子应用传递参数。我最早觉得这个 props 就是用来传个基座地址、传个环境变量什么的后来发现这套机制能做的事比想象中多。主应用里注册微应用时加一个 props 字段// 主应用 main.js import { registerMicroApps, start } from qiankun; registerMicroApps([ { name: user-app, entry: //localhost:7101, container: #subapp-container, activeRule: /user, props: { appBase: /user, env: production, userInfo: { name: 张三, token: xxx } } } ]); start();子应用这边在生命周期函数里接收// 子应用 user-app 的 main.js export async function mount(props) { console.log(子应用收到 props:, props); console.log(基础路径:, props.appBase); console.log(用户信息:, props.userInfo); // 通常子应用用 webpack 打包可以用 props 里的参数配置路由 base render(props); }这套 props 通信最大的特点是初始化即快照。意思是说这些参数是子应用启动那一刻传进去的子应用内部如果要用它做初始化配置非常合适。比如我实际项目里就喜欢把主应用计算好的路由前缀传进去这样子应用内部的路由 base 不用写死主应用改挂载路径时子应用不用跟着改。但要注意的是props 本身没有响应式能力。它是单向的、一次性的。如果主应用的 userInfo 在用户登录之后变了子应用里的 props.userInfo 并不会自动更新。有人会问那我把 props 传一个引用类型对象主应用改对象里的字段子应用是不是就能感知到其实可以但前提是共享的都是同一个引用。不过这样做很不可控因为 JS 沙箱会把子应用的 window 隔离你很难保证这个引用在所有环境下都一样。所以在我的经验里props 更适合传启动时就要定下来且不再变的东西比如路由前缀、环境标识、主应用版本号这些偏静态的配置。再补充一点props 也并不是只能传一个普通对象。你完全可以在 props 里塞函数这就打开了自研通信的大门后面讲事件总线时我会专门说这个。2.2 GlobalState官方给的伪全局状态qiankun 框架内置了一个全局状态管理能力叫 GlobalState官方文档里有专门章节。它的设计思路是主应用定义一个全局的 state主应用和子应用都能订阅这个 state 的变化也都能修改这个 state。用法如下// 主应用 main.js import { initGlobalState } from qiankun; // 定义全局状态 const actions initGlobalState({ userInfo: { name: , token: }, theme: light, unreadCount: 0 }); // 主应用订阅状态变化 actions.onGlobalStateChange((state, prevState) { console.log(主应用监听 global state 变化:, state); }); // 主应用更新状态 actions.setGlobalState({ userInfo: { name: 张三, token: abc123 } }); // 主应用把方法传给子应用 registerMicroApps([ { name: user-app, entry: //localhost:7101, container: #subapp-container, activeRule: /user, props: { actions } } ]);子应用这边在 mount 生命周期里取到 actions// 子应用 user-app 的 main.js let globalActions; export async function mount(props) { globalActions props.actions; // 子应用订阅全局状态变化 globalActions.onGlobalStateChange((state, prevState) { console.log(子应用监听 global state 变化:, state); }, true); // 子应用也可以修改全局状态 setTimeout(() { globalActions.setGlobalState({ unreadCount: 5 }); }, 2000); render(props); }看上去很方便但实际用起来有几个点要特别留意。第一个setGlobalState做的是浅比较。每调一次qiankun 会把你传进去的新 state 和当前 state 做一层浅层对比如果对比下来没有变化是不会触发 onGlobalStateChange 回调的。换句话说如果你要更新的状态是个嵌套很深的复杂对象直接改对象内部某个字段再把它塞回去很可能因为浅比较看到的还是同一个引用导致子应用那边收不到更新通知。我记得第一次遇到这个坑的时候调了半天最后打开 qiankun 源码一看才明白。举个例子假如全局 state 长这样const state { user: { name: 张三, profile: { age: 28 } } };你这样做不会触发更新actions.setGlobalState({ user: state.user // 还是同一个引用 });正确做法是创建一个新对象actions.setGlobalState({ user: { ...state.user, profile: { ...state.user.profile, age: 29 } } });这其实是 JavaScript 对象不可变更新immutable update的思路跟 React 的 setState 一样开发者要自己保证每次更新的对象和原来的引用不同。第二个GlobalState 适合传递状态量不适合传递事件流。状态量意思是像用户信息、主题色、未读数这样长时间存在、需要多个应用共同维护的数据。而事件流是那种发生了什么事情的瞬时通知比如订单提交成功列表需要刷新。GlobalState 虽然也能模拟事件做法是加一个带时间戳的字段每次事件都更新这个字段但这样用非常别扭不如直接用事件总线。所以我建议把 GlobalState 定位成跨应用共享状态而不是跨应用通知。第三个也是容易忽视的onGlobalStateChange注册的回调函数在子应用卸载时不会自动清除。如果子应用频繁的挂载和卸载每次都注册新回调会导致回调越积越多状态一变主应用和其他子应用被重复通知。后面讲坑的时候我会再展开说。2.3 官方方案的局限为什么很多人选择自研讲完官方这套东西你会发现一个问题官方提供的通信能力本质上是主应用中心化的。主应用定义一个 state所有子应用围绕这个 state 来同步数据。这确实能满足一部分场景但真正的业务里子应用和子应用之间的通信需求往往更加动态比如子应用 A 完成某个操作后子应用 B 要刷新数据A 并不需要知道 B 内部怎么处理只想发一个数据已变化的通知。主应用和子应用之间需要双向通信子应用也可能要主动通知主应用做某件事比如打开弹窗、跳转路由。业务方希望通信代码能复用最好在子应用独立运行时不经过主应用启动也能正常工作方便本地开发。这些需求用 GlobalState 也能硬做但代码会变得绕类型提示也基本没有。所以我在实际的项目里倾向于把官方方案当作地基在这个基础上自己做一层轻量的事件总线封装。这也是接下来重点要讲的内容。3. 升级方案自研一个跨应用事件总线才是真正常用的一招3.1 为什么事件总线比 GlobalState 更适合业务通信事件总线的核心概念其实很简单就是发布订阅模式。有一个公共的消息中心应用 A 往里面发一条消息应用 B 往里面注册一个监听函数A 发消息时 B 的监听函数就会被调用。这个模式在单体应用里已经很成熟了比如 Vue 里的 EventBus、Node 里的 EventEmitter在微前端架构里思想完全一样区别只在于这个消息中心放在哪里、怎么让所有应用共享到它。对比 GlobalState事件总线最大的优势在于解耦。发送方只需要关心我要发什么消息不关心谁会接收、接收之后做了什么。接收方也只需要关心我要听什么消息不关心消息是谁发的。这种松耦合关系在微前端这种多团队协作的场景下特别重要。你想一下订单子应用和商品子应用分别由两个团队维护他们要协调库存变更的逻辑用 GlobalState 的话需要约定一个共同的 state 结构两个团队都要对着同一份结构改很容易因为某一方的失误影响全局而用事件总线订单团队只需要发一条inventory:changed的消息商品团队去订阅这条消息两边只约定一个消息名边界清晰多了。另外事件总线传递的粒度更细。一条消息可以携带任意结构的数据也可以不带数据只做一个通知。而对消息的响应是即时的不需要像 GlobalState 那样去对比状态值的变化。3.2 手写一个几十行的事件总线还是直接用 Mitt事件总线的事件中心虽然可以自己写但我更推荐直接用现成的库。qiankun 社区里很多人用mitt它是一个只有 200 字节左右的小库API 很简洁支持on、off、emit三个方法也支持通配符*监听所有事件。我用下来觉得它比 Node 内置的 EventEmitter 更适合前端因为体积小、API 简单、TypeScript 类型支持也不错。安装很简单npm install mitt然后在你想要的位置创建一个公共文件。关键问题来了这个文件放在哪3.3 两种放置策略放进主应用 vs 发布成独立模块第一种做法是把事件总线实例放进主应用通过 props 传给每个子应用。这种做法最简单适合主应用和子应用在一个仓库、一个团队维护的场景。// 主应用引入 mitt 并创建事件总线 import mitt from mitt; export const eventBus mitt(); // 注册子应用时通过 props 传入 registerMicroApps([ { name: user-app, entry: //localhost:7101, container: #subapp-container, activeRule: /user, props: { eventBus } }, { name: order-app, entry: //localhost:7102, container: #subapp-container, activeRule: /order, props: { eventBus } } ]);子应用在 mount 时接收// 子应用 user-app export async function mount(props) { const { eventBus } props; // 订阅事件 eventBus.on(order:created, (payload) { console.log(收到订单创建通知:, payload); // 执行业务逻辑比如刷新商品列表 }); // 发送事件 eventBus.emit(user:login, { name: 张三, token: xxx }); render(props); }第二种做法是把事件总线抽成一个独立的 npm 包主应用和子应用都依赖这个包。这种做法在主应用和子应用是不同仓库、不同团队独立开发的场景下更合适。因为子应用通常也是独立部署的它们在本地开发时可能根本不会启动主应用而是自己单独跑起来调试。如果事件总线实例只存在于主应用里子应用一脱离主应用就跑不起来这违背了微前端每个应用可独立开发的初衷。发布成独立包以后子应用单独运行时可以直接用包里的 eventBus嵌到 qiankun 里时也可以用同一个实例。但要小心一个细节这个包必须在主应用和子应用里共享同一个模块实例。换句话说如果主应用和子应用都分别打包了一份自己的 mitt 实例它们之间就还是两个事件中心消息互相听不到。提示解决同一个模块只保留一份实例的常见手段是把事件总线实例挂到 window 上因为在 qiankun 沙箱开启时虽然每个子应用的 window 是代理过的但主应用和子应用仍然能访问同一个宿主 window 上的同一份对象。这也是 qiankun 官方全局状态实现的核心逻辑。这个细节我在后面的坑部分还会再提到。3.4 给事件总线加上命名空间、类型提示和调试开关实际业务里事件总线用起来比上面这个简单例子要复杂得多。如果把所有业务事件都平铺在一个事件总线里时间一长消息名就乱成一锅粥。我自己的一个小习惯是给事件名约定统一的命名规则用业务域:动作:目标的格式比如order:created:refresh、user:login:sync、cart:updated:badge。这样看名字就知道消息是哪个业务域发出的、要干什么。如果你用 TypeScript可以给 mitt 实例加上泛型这样事件名和 payload 类型都能有提示// communication.ts import mitt from mitt; type Events { user:login: { name: string; token: string }; user:logout: void; order:created: { orderId: string; amount: number }; inventory:changed: { productId: string; stock: number }; }; const eventBus mittEvents(); export default eventBus;这样在发送和订阅时事件名和 payload 都能自动提示能避免大量手写字符串导致的低级错误。调试开关也值得做。因为微前端应用间通信的链路不像单体应用那样直观消息发出去了、但是没收到排查起来非常费劲。我通常在事件总线上包一层打印所有 emit 和 on 的记录方便开发时观察// debug 中间件示例 import mitt from mitt; export function createDebugEventBus(enable: boolean) { const bus mitt(); return { on(type, handler) { if (enable) { console.log([EventBus] subscribe: ${type}); } bus.on(type, handler); }, emit(type, payload) { if (enable) { console.log([EventBus] emit: ${type}, payload); } bus.emit(type, payload); } }; }这个 debug 开关可以跟随环境变量控制生产环境关掉开发环境打开。调试跨应用问题时你只需要在主应用和子应用的 console 里同时观察同一份 EventBus 日志就能快速定位消息是哪一端没发出去、哪一端没收到。3.5 事件总线的局限它只做通知不做状态事件总线的天然弱点是不存状态。消息发完就结束了如果某个应用是在消息发出之后才挂载的那它永远收不到之前发出的消息。所以对于后来者也需要拿到最新状态的场景必须配合一个能存状态的东西使用。举个例子用户登录成功后主应用发了一条user:login事件此时用户子应用还没挂载等它挂载后它只能通过 props 或者 GlobalState 去拿最新的 userInfo事件总线帮不了它。所以我的通用做法是状态用 GlobalState 或 storage 存动作用事件总线通知两个配合使用各管一摊效果最好。用事件总线发状态已变化的通知用 GlobalState 放变化后的具体状态值。4. 场景化通信设计与方案选型4.1 用户登录态同步架起主应用与子应用之间的桥梁用户登录态同步是微前端里最典型、最普遍的通信场景。通常主应用负责承载登录页登录成功后要和所有子应用同步用户信息。我的推荐做法是组合拳主应用用自定义事件广播用户已登录这个事件同时把完整的 userInfo 写进 localStorage。主应用登录成功后的代码逻辑// 主应用 import { eventBus } from ./communication; function handleLoginSuccess(userInfo) { // 1. 写入 localStorage方便新挂载的子应用获取 localStorage.setItem(global_user_info, JSON.stringify(userInfo)); // 2. 发送事件通知所有当前已挂载的子应用 eventBus.emit(user:login, userInfo); }子应用在 mount 时先读一次 localStorage再订阅事件// 子应用 export async function mount(props) { const { eventBus } props; // 首次加载从 localStorage 拉取 const raw localStorage.getItem(global_user_info); if (raw) { store.userInfo JSON.parse(raw); } // 之后订阅登录事件实时更新 eventBus.on(user:login, (userInfo) { store.userInfo userInfo; // 更新界面 renderApp(); }); render(props); }这套组合的好处是已经挂载的子应用能瞬间收到登录通知后挂载的应用也能从 localStorage 里读到最新的用户信息两头都不漏。4.2 子应用向主应用汇报事件切换路由也能联动微前端场景里子应用内部路由切换后主应用的菜单高亮、面包屑、标题都要跟着变这是另一个高频需求。做法是子应用在路由变化时向事件总线发一条路由变更消息主应用订阅后更新对应的 UI 状态。子应用里在路由守卫中发送// 子应用 router.js以 Vue Router 为例 router.afterEach((to) { const { eventBus } window.__POWERED_BY_QIANKUN__ ? props : localEventBus; eventBus.emit(route:change, { appName: user-app, path: to.path, title: to.meta.title }); });主应用订阅// 主应用 eventBus.on(route:change, ({ appName, path, title }) { // 更新面包屑 breadcrumbStore.update(title); // 更新菜单高亮 menuStore.setActive(appName, path); });注意这里有个细节子应用在单独调试时window.__POWERED_BY_QIANKUN__是 false此时要使用子应用本地的事件总线实例保证它独立运行时也能通过本地路由驱动自己的 UI。只有真正被 qiankun 加载的时候才使用主应用传下来的 eventBus。4.3 子应用之间的业务协作用事件名约定而不是共享状态两个子应用之间的通信是最复杂的。假设订单子应用创建了一笔订单库存子应用需要减库存。这种场景我的建议是千万别共享一个状态对象让两边同时读写而是要定义清晰的事件名和 payload让订单应用只负责发消息库存应用只负责消费消息。订单子应用创建订单成功后// 订单子应用 eventBus.emit(order:created, { orderId: SO20240101001, amount: 199.99, products: [{ productId: P001, quantity: 2 }] });库存子应用订阅并处理// 库存子应用 eventBus.on(order:created, ({ products }) { products.forEach(({ productId, quantity }) { // 调用库存接口减库存 inventoryApi.decrease(productId, quantity); }); });这里要注意的一点是事件总线上的消息是无状态、不重放的。如果两个子应用不是同时挂载的库存子应用没有监听到那条订单事件订单子应用也不知道。所以在关键业务上最好还是要把核心数据落到后端事件只作为一个触发刷新的信号真正要展示的数据都通过接口从后端获取。事件总线的定位是触发动作而数据的唯一来源依然是后端这一点要牢记。4.4 基于 localStorage 和 CustomEvent 的轻量方案除了事件总线和 GlobalState还有一个在很多老项目里常见的方案利用 localStorage 存储数据配合浏览器原生的 window 事件触发通知。一个典型的写法// 发送方 function sendData(key, data) { localStorage.setItem(key, JSON.stringify(data)); // 发一个自定义事件通知同源的其他标签页/挂载的应用 window.dispatchEvent(new CustomEvent(storage-change, { detail: { key, data } })); } // 接收方 window.addEventListener(storage-change, (event) { const { key, data } event.detail; if (key cart:updated) { // 更新购物车角标 cartStore.update(data); } });这个方案好处是零依赖、不用创造额外的事件中心适合轻量级场景。但它有两个需要小心的点localStorage 本身在设计上是给同源下的所有标签页共享的存储事件在同一个标签页里监听是不生效的所以必须配合window.dispatchEvent(new CustomEvent(...))手动派发。沙箱环境下子应用访问的 window 可能是代理后的你在子应用里存 localStorage和主应用里存的其实还在同一个浏览器存储空间所以数据能通但要注意命名冲突。我给 key 加前缀的习惯就是从这里来的比如app:user:info、app:cart:count避免不同应用把同一个 key 覆盖来覆盖去。从我个人经验看localStorage 方案更适合传递不敏感、非高频的状态数据比如主题配置、默认推荐位 ID、活动弹窗展示次数等。高频业务事件还是交给事件总线更顺手。4.5 各方案怎么选一张对比表说清楚我把上面讲到的几种方案从适用场景和优缺点维度做了一张对比表方便你决策通信方案适用场景优点需要注意的地方生命周期 props主应用向子应用传启动配置简单、直接、类型安全单向且基本一次性状态变化不自动同步GlobalState跨应用共享可变更的状态官方内置、响应式更新浅比较容易踩坑嵌套对象要手动不可变更新事件总线mitt/自研业务动作通知应用之间解耦松耦合、支持双向、可带任意数据不保存状态后挂载的应用收不到历史事件localStorage CustomEvent轻量状态共享、跨标签页零依赖、简单手动派发事件命名空间需要规范路由参数传值跳转场景传递上下文直观刷新不可变需要各应用路由协作传大量复杂对象不方便另外说一句生产环境的大型微前端项目里这些方案通常不是互斥的而是共存互补的。我自己的项目里就是启动配置走 props跨应用共享状态走 GlobalState localStorage业务动作通知走自研事件总线页面跳转参数走路由。每套机制解决一类特定问题不要试图用某一种方案解决所有通信需求这是微前端架构里的一个核心设计原则。5. 实战中高频踩坑与排查方法5.1 坑一子应用事件的重复订阅导致消息执行多次这个是我见过最多的问题也是微前端通信最典型的坑。子应用被多次进入、离开每次 mount 时都调用一次eventBus.on(...)但 unmount 时没有调用off(...)把监听函数移除结果就是消息越积越多发一条消息监听函数被执行三五次。避免方法很简单在两个层面做规范一是子应用卸载时手动取消订阅。在事件总线里建议把订阅函数的取消句柄保存起来// 子应用 let unsubscribe; export async function mount(props) { const { eventBus } props; const handler (payload) { console.log(收到消息, payload); }; eventBus.on(order:created, handler); // 保存取消函数 unsubscribe () eventBus.off(order:created, handler); } export async function unmount() { // 卸载时取消所有订阅 unsubscribe unsubscribe(); }二是从根上规避重复。在订阅之前先取消旧订阅再建立新订阅。如果事件总线是自己封装的可以做一个防重复订阅的逻辑每次都替换对应事件的 handler// 封装同一个事件只保留最新一个 handler function subscribe(type, handler) { bus.off(type, existingHandler); bus.on(type, handler); }这个逻辑虽然简单但在实际项目里能省下不少排查时间。5.2 坑二qiankun 沙箱环境下全局变量到底是哪一份qiankun 在开启沙箱的时候每个子应用访问到的 window 都是一个 Proxy 代理。你在子应用里写window.foo 1这个操作可能被代理到子应用自己的沙箱环境里不会修改真正的宿主 window。这就带来一个直接后果如果子应用 A 通过事件总线把一些信息挂到window.__shared__上子应用 B 去读的时候可能读不到。我在一次跨应用传递路由联动信息时遇到过这个问题。主应用往 window 上挂了一个对象子应用的代码通过 window 去读结果得到的要么是 undefined要么是代理前的旧值花了大半天才定位到是沙箱隔离导致的。解决方案有两个方向尽量不用全局对象去存共享数据改用官方提供的 GlobalState 或者事件总线实例传递因为这些机制内部已经处理好了跨沙箱的共享。如果你一定要用 window 上的某个变量来共享数据那就要确保写入方是主应用或者确保你写入的是同一个宿主 window 上的同一个属性而不是子应用沙箱代理出来的那个 window。更稳妥的做法是像 4.4 节里提到的用 localStorage 而不是全局变量来共享数据避开沙箱隔离的雷区。5.3 坑三GlobalState 浅比较导致子应用收不到更新前面提到过 GlobalState 的浅比较机制我再详细说一下具体表现和解决思路。假设你有一个全局状态initGlobalState({ user: { name: 张三, tags: [vip] } });在子应用里你这样做const state actions.getGlobalState(); state.user.tags.push(newUser); actions.setGlobalState(state); // 没反应因为state.user还是原来的对象引用浅比较认为没有变化所以不会触发订阅回调。你需要在更新时生成一个新的 user 对象const state actions.getGlobalState(); actions.setGlobalState({ ...state, user: { ...state.user, tags: [...state.user.tags, newUser] } });每次更新都严格按照不可变更新来写别偷懒直接改深层字段。这个习惯在微前端下比在普通 React/Vue 项目里更要命因为变更触发链变长了涉及多个应用一旦触发不了排查范围会非常大。5.4 坑四子应用独立运行和嵌入运行时通信要兼容微前端项目里几乎每个子应用都要求能独立运行、独立开发。子应用独立运行时没有主应用传下来的 props也就没有 eventBus那一堆用了 eventBus 的代码就会直接报错。解决办法是给子应用封装一个通信兜底机制。拿事件总线举例// 子应用的通信工具模块 import { eventBus as localBus } from your-communication-package; let bus; export function getBus(props) { if (bus) return bus; // 优先使用主应用传递的事件总线 if (props props.eventBus) { bus props.eventBus; } else { bus localBus; // 独立运行时使用本地实例 } return bus; }这样无论是独立运行还是嵌入主应用子应用的业务代码都可以正常调用通信 API不需要感知自己跑在什么环境里。对本地的 mock 数据、样式调试都友好。5.5 排查思路先打印再二分最后看源码如果你也遇到发消息没反应这种问题我的排查流程是固定的先打开主应用和子应用的控制台在通信模块里打印所有 emit 和 on 的日志确认消息是否真的发出、是否真的有人订阅。很多时候问题出在事件名拼写不一致这种低级错误最隐蔽也最消耗时间。如果日志显示有订阅、有发出执行却没有反应就去检查订阅回调函数内部有没有抛异常。因为在事件总线里如果某个订阅者的 handler 抛错可能导致整个消息分发逻辑中断这取决于你的实现方式其他订阅者的回调也会被连带影响。如果事件名和 handler 都没问题再去看是不是重复订阅、是不是同一个事件被订阅了很多次但每次都只有最新一个 handler 起效。实在查不出来时直接去看 qiankun 的源码关注initGlobalState、setGlobalState这两个方法的实现以及沙箱代理对全局对象的影响。这套流程用下来绝大多数通信问题都能在半小时内定位。关键还是每次加代码时都留好日志别等到出问题了再满世界找线索。6. 最后分享一点我自己沉淀下来的经验qiankun 应用间通信这件事做到最后你会发现它考验的不是某个框架 API 的使用熟练度而是你对应用边界和数据流的理解。我自己的体会是微前端架构下的通信设计一定要在项目开始时就把规矩定好而不是等到联调阶段才临时想办法第一统一通信入口。无论用事件总线还是 GlobalState都要有专门的文件或者包来统一管理做到所有通信逻辑在一个地方维护。不要在主应用的组件里直接写裸的 localStorage.setItem也不要在子应用的业务代码里到处 new EventBus时间一长就没人能说清消息是从哪来的了。第二定好事件名和数据结构规范。消息名用业务域:动作:目标的格式payload 统一为对象结构字段命名用驼峰。如果团队还有余力考虑维护一份通信事件清单文档把所有跨应用事件名、用途、payload 结构、发送方、接收方列清楚。这在多团队协作时特别有用比在微信群里发一段消息靠谱得多。第三做好兼容和降级。子应用独立运行时的兜底能力、事件总线不可用时的降级方案、以及重复订阅的防抖处理这些都要在封装层解决不要散落到每个业务页面里。业务代码写得再乱只要通信基础稳固项目就不会出大乱子。最后再提醒一个点微前端应用间通信虽然能解决很多问题但并不是所有数据都适合传到前端通信链路里来。重要的业务数据一定要走后端接口前端通信链路只负责触发刷新和轻量上下文传递。这一点在架构设计阶段就要说清楚否则等业务规模变大通信链路上的消息会膨胀到你根本没法维护的程度。这个分寸感就是微前端架构师和普通前端开发之间最大的区别。
返回列表