多彩编程 多彩编程MZPH · CODE BLOG
ARTICLE DETAIL

文章详情

深耕前端与后端开发技术的一线实战笔记与踩坑复盘。

微前端基座Store实战:基于Vue3+Vite+Qiankun的跨应用状态管理方案

微前端基座Store实战:基于Vue3+Vite+Qiankun的跨应用状态管理方案 大概是从单体应用切到微前端架构之后大家最先欢呼的是子应用终于可以独立开发、独立部署了但用不了两周就会碰到一个特别现实的问题登录态在A应用里是好的切到B应用就丢了用户在设置页改了主题色回到首页又变回默认几个子应用同时要读某个全局配置结果每个应用各存一份改起来要全量发版。我把话放这儿——微前端改造里让人头秃的不是路由冲突、不是样式隔离恰恰就是这个store的问题。这篇内容就是把我在vue3 vite qiankun这套组合下做微前端基座store的完整过程、方案取舍、踩坑记录都摊开来讲适合正在做微前端改造、或者准备搭微前端基座的前端团队参考。1. 微前端里的状态管理问题先于你踩的第一脚坑出现1.1 从单应用时代到微前端时代store的边界被打破了先说个背景。单体应用时代store是天然的全局单例不管是Vuex还是Pinia整个应用共享一份内存状态组件树的任何一个地方都能读到、改到同一份数据。这个心智模型延续了快十年大家已经习惯了状态就是全局的从来没想过有一天全局这个词本身会被拆掉。到了微前端架构里基座和子应用各自有独立的Vue实例、独立的Pinia实例它们的内存空间天然是隔离的。我见过不少团队第一次联调时发出灵魂拷问为什么我在基座里setState子应用里一点反应都没有原因就是两个应用各玩各的store谁也没资格说自己是全局唯一的那个store。这时候就需要一个东西把全局重新立起来这就是基座store要干的事。它本质上不是某个具体的库而是一种跨应用状态协调机制基座应用作为状态的所有者和分发者把各个子应用需要的公共状态统一收口再通过某种通道同步给子应用。1.2 基座store和单应用store的四个本质差异把基座store当普通store来写是很多微前端项目后续维护成本爆炸的根源。我整理了四个核心差异理解了这四点后面所有设计决策都有依据了。维度单应用store基座store生命周期跟随整个应用创建一次销毁一次跟随基座但子应用会反复挂载卸载store的订阅者频繁变化可见性只在应用内部组件通过注入访问必须显式暴露给外部子应用需要定义明确的读写协议同步方式依赖响应式系统自动收集依赖跨应用边界只能靠事件、共享引用或桥接层显式同步隔离性不需要考虑多应用并发读写多个子应用可能同时读写同一份状态必须有约定和约束其中最要命的是第二点——可见性。单应用store你随便写重构也无所谓反正只有自己人调用。基座store是给外部用的它实际上变成了基座和所有子应用之间的一个公开契约。一旦子应用接入并依赖了store里的某个字段你后续想改字段名、改嵌套结构就得把所有子应用都翻一遍这跟改动一个公共npm包的API是一个性质。2. 设计基座store前先把全局状态这个词拆开2.1 三层状态模型全局态、子应用态、临时态我在做基座store之前先做了一件看起来跟代码没关系的事把整个系统的状态按照作用域分成三层然后挨个问自己这一层到底需不需要跨应用共享。全局态跨应用用户登录信息、Token、权限码、主题配置、语言偏好、系统级开关。这一层的特点是所有子应用都要用而且必须保持强一致。这部分理所当然进基座store。子应用态应用内订单列表、表单草稿、组件显隐、页面级筛选条件。这一层是子应用自己的业务状态留在子应用自己的Pinia里就行千万别往基座塞。临时态页面/组件内弹窗开关、下拉菜单状态、路由参数。这一层连Pinia都不用组件内部或路由驱动就够了。我见过一个反面案例某团队为了让所有东西都集中管理把子应用的列表数据、筛选条件全放进了基座store。结果子应用每次加载都要从基座拉一大坨数据切换子应用时基座store里攒了一堆别的应用的数据调试时根本分不清哪条是当前的、哪条是残留的。这种全局化不是架构升级是给自己埋雷。2.2 宁小勿大原则为什么状态越少系统越稳拆完三层模型之后基座store的边界其实已经很清晰了剩下的问题是一个相反方向的担心这也不敢放、那也不敢放基座store会不会太空我的答案是空就对了。基座store的设计准则不是能放就放而是不得不放才放。原因有两个都很实际。第一是耦合度。每往基座store里多放一个字段就相当于在基座和子应用之间多签了一条契约。契约越多后续变更时的沟通成本和回归成本就越高。子应用本来应该是一个可以独立迭代、随时替换的单元结果因为store里塞了太多它的专属状态替换时还得先清理一堆状态残留微前端的核心收益直接打折扣。第二是通信成本。微前端跨应用状态同步本质上比单应用内部的内存访问要重得多。qiankun的globalState每次更新会遍历所有订阅者事件总线要逐个派发共享引用方案虽然轻但也会触发一系列响应式依赖更新。状态越大越碎这些同步任务的频率就越高页面越容易感觉到卡顿。所以基座store瘦身不是洁癖问题是性能和可维护性的双重需求。3. 三种主流基座store方案我为什么选了qiankun pinia路线3.1 qiankun官方initGlobalState最省事但别硬塞qiankun从很早就提供了官方全局状态方案initGlobalState。基座里初始化一份state子应用里用onGlobalStateChange接收变更、用setGlobalState提交变更。这套方案的优点是零额外依赖、跟qiankun本身的生命周期绑定比较正统。但它有几个限制需要提前认清状态必须可序列化。globalState内部有快照机制函数、DOM对象、组件实例这类东西塞进去再取出来很可能已经不是原来那个东西了。更新粒度比较粗。setGlobalState做的是浅合并和全量通知任何一个字段变了所有订阅了全局状态的子应用都会收到整份state的变更回调。也就是说它不是精细到字段级的精确派发。没有类型体系。原生API的state是普通对象子应用拿到的也是普通对象类型提示基本靠手写接口这在TS项目里体验算不上好。所以官方方案适合状态量少、结构简单、对类型要求不高的场景。我刚接手项目时就是用这个方案起步的简单场景完全够用问题出在状态一多、字段一嵌套子应用里做状态判断的代码就开始变得很啰嗦。3.2 EventBus事件总线灵活过头容易埋雷还有一个常见思路是在基座里维护一个全局事件总线子应用通过emit触发事件、通过on监听事件把共享状态转成共享消息。这个方案的优点是极其灵活事件名随便定、参数随便传不需要像globalState那样受序列化限制函数也能当参数扔过去。我最初也动过这个念头因为它看起来完美解决了globalState的痛点。但实际做下来会发现事件总线是把复杂度从数据流转移到了消息流而消息流是更难把控的。事件名谁定义参数结构谁保证监听器谁来注销一旦某个子应用崩溃或未正常卸载它的监听器还挂在总线上下一次同名事件触发时就会去调一个已经销毁的组件方法轻则报错重则内存泄漏。做了一段时间以后总线上的事件越来越多命名冲突、数据歧义、调试困难这些问题全来了。所以EventBus更适合做一次性动作通知比如用户已登出请各应用清理本地状态不适合当状态同步的主通道。3.3 共享响应式实例pinia直接注入子应用我最终的选择最终我采用的是第三条路把基座应用里创建好的Pinia实例通过qiankun的props机制直接传给子应用子应用拿到基座Pinia后直接读写基座定义的store。核心思路就一句话与其把状态倒到某个通用的全局对象里让子应用去读不如直接把store本身共享出去子应用里用的就是基座那份响应式状态天然响应式、天然带类型、天然能感知变更。这个方案的好处非常明显类型友好。基座store是用TS写的子应用引入store类型后字段名、方法签名全都有提示改字段名时编译器能帮忙查漏补缺。响应式原生。子应用直接读基座store的ref或state修改后基座和所有子应用自动响应不需要额外的同步桥。开发体验统一。子应用写状态的方式和写自己的Pinia没有任何区别团队成员几乎零学习成本。当然它也有代价基座和子应用的耦合更深了子应用不再是一个完全独立的个体它运行的前提是基座必须提供一个满足约定的Pinia实例。我的判断是对于一个统一规划、统一技术栈的微前端体系来说这个代价可以接受如果你们是跨团队、跨技术栈的开放平台那还是回到globalState更稳妥。这里需要特别说明一下共享Pinia实例和全局变量完全不同。Pinia实例虽然共享了但每个store内部的定义、状态结构、API依然由基座统一管理子应用并不是随手往全局对象上挂东西而是通过基座暴露的store来访问状态。这样的乱挂风险被控制在了store这个抽象层级内部。4. vue3 vite qiankun 的基座store落地实现4.1 基座侧定义一个面向全局共享的pinia store先看基座侧。假设技术栈是Vue3 Vite TS基座里用Pinia定义一个全局共享store。这个store的定义跟写普通store没有区别关键在于哪些内容放进来是经过2.1节那套模型筛选的只保留真正的全局态。// 基座代码src/stores/global.ts import { defineStore } from pinia interface UserProfile { id: string name: string roles: string[] } export const useGlobalStore defineStore(global, { state: () ({ token: localStorage.getItem(__mfe_token__) || , user: null as UserProfile | null, theme: light as light | dark, locale: zh-CN, // 全局配置项 systemName: 某某管理平台, featureFlags: {} as Recordstring, boolean, }), actions: { setToken(token: string) { this.token token localStorage.setItem(__mfe_token__, token) }, setUser(user: UserProfile) { this.user user }, setTheme(theme: light | dark) { this.theme theme document.documentElement.setAttribute(data-theme, theme) }, logout() { this.token this.user null localStorage.removeItem(__mfe_token__) }, }, })注意我在setToken和logout里同步了localStorage。这里有个从单应用时代保留下来的好习惯关键状态要双写。内存里的store负责当前会话的一致性localStorage负责跨刷新、跨标签页的恢复。基座store的加载阶段需要从localStorage里恢复初始值否则刷新页面后store会回到初始态表现为登录状态莫名其妙丢了。4.2 桥接层把pinia状态同步进qiankun globalState虽然我已经在用共享Pinia实例方案但实际的基座store落地里我依然保留了一层qiankun globalState的桥接。为什么因为不是所有子应用都适合拿基座Pinia。比如后来接入的一个老项目还是Vue2 Webpack让它拥抱共享Pinia实例改造成本实在太高。对这种子应用走globalState反而是最平滑的接入方式。所以实际落地是双轨制核心子应用新项目Vue3 TS直接共享Pinia实例历史子应用老项目通过qiankun官方globalState对接同一份数据源。为此我封装了一个桥接层本质是一个store的变更自动同步到另一个store。// 基座代码src/mfe/storeBridge.ts import { initGlobalState, MicroAppStateActions } from qiankun import { watch } from vue import { useGlobalStore } from ../stores/global const store useGlobalStore() // 需要暴露给globalState通道的字段做一个显式映射 const stateToGlobal () ({ token: store.token, user: store.user, theme: store.theme, locale: store.locale, systemName: store.systemName, featureFlags: store.featureFlags, }) let syncing false const globalActions: MicroAppStateActions initGlobalState(stateToGlobal()) // pinia - globalState基座store变化时推送最新值 watch( () [store.token, store.user, store.theme, store.locale], () { if (syncing) return globalActions.setGlobalState(stateToGlobal()) }, { deep: true } ) // globalState - pinia子应用通过globalState提交变更时写回基座store globalActions.onGlobalStateChange((state) { if (!state) return syncing true if (state.token ! undefined) store.token state.token if (state.user ! undefined) store.user state.user if (state.theme ! undefined) store.setTheme(state.theme) if (state.locale ! undefined) store.locale state.locale syncing false }) export function updateGlobalState(partial: Recordstring, unknown) { // 内部调用时走统一入口触发pinia - globalState Object.assign(store.$state, partial) }这段代码里的syncing标志很关键它用来打断pinia变更 → globalState回调 → 写回pinia → 又触发watch → 又setGlobalState的无限循环。qiankun的globalState回调是同步触发的如果不加这个互斥标志一次状态更新就能引发几十次无效的往返同步。4.3 子应用侧读、写、监听全局状态的三件套子应用侧分两种情况。如果是Vue3新项目我直接在基座通过props把pinia实例传下去子应用注册它我直接共享基座的 Pinia 实例。// 基座代码main.ts 中注册子应用 import { registerMicroApps, start } from qiankun import { pinia } from ./stores registerMicroApps([ { name: child-app-a, entry: //localhost:7101, container: #micro-container, activeRule: /app-a, props: { sharedPinia: pinia, // 传给子应用的共享Pinia }, }, ]) start()子应用拿到sharedPinia后在自己的应用初始化时把它注册为默认的Pinia实例// 子应用代码main.ts import { createApp } from vue import { createPinia } from pinia import App from ./App.vue let app: ReturnTypetypeof createApp | null null export async function mount(props: { sharedPinia?: ReturnTypetypeof createPinia }) { app createApp(App) // 有基座传入的共享pinia就用共享的否则自己创建一个兜底 app.use(props.sharedPinia || createPinia()) app.mount(#app) } export async function unmount() { app?.unmount() app null }这样子在子应用组件里useGlobalStore()拿到的就是基座那份store读取、修改、监听全都和本地store的写法一模一样。对于Vue2老项目就走qiankun的props globalState那一套基座bridge层已经帮忙对接好了子应用只需要// 老项目子应用代码qiankun接入时注册全局状态监听 export function mount(props: any) { props.onGlobalStateChange?.((state: any) { // 将全局状态同步到子应用自己的Vuex/localstate store.commit(setGlobalState, state) }) }5. 状态同步、销毁与写入冲突实测翻车的四个细节5.1 循环更新你改我、我改你、大家一起改第一个坑就是循环更新。我们项目的场景是这样子应用A里有个切换主题按钮用户点了以后子应用A调setGlobalState把主题改成dark基座的bridge层监听globalState变更把值写回pinia storestore的watch发现theme变了又触发setGlobalState推送最新值于是子应用A的onGlobalStateChange回调再次执行又把同一份值set了一遍。这种循环在状态值发生实际变化时通常一两次就停了最怕的是中间有人不小心改了引用地址——比如每次setGlobalState时新建一个{ token, user, theme }对象即使里面内容没变qiankun内部也可能认为有变更于是一次无意义操作变成无限循环控制台直接刷屏。解决办法就是上面代码里那个syncing互斥标志再配合set之前先比较、变化才推送的习惯// 基座 bridge 里更稳的写法示例 globalActions.onGlobalStateChange((state) { if (!state || syncing) return syncing true // 只处理真正有变化的字段 if (state.theme state.theme ! store.theme) { store.theme state.theme } syncing false })原则只有一条任何一端的变更回调被触发后先把互斥标志打开再做事做完立即关闭而且是值变了才写不要无脑把对方推来的值原样写回去。5.2 子应用卸载后监听器不清理的后果第二个坑是监听器不清理。qiankun的globalState设计里onGlobalStateChange返回一个取消订阅函数但我见过很多项目在mounted里注册了监听unmount里却忘了调用offGlobalStateChange。后果是什么子应用卸载了它的监听函数还被基座globalState保存在订阅者列表里。当下一次某个子应用setGlobalState时这个已经卸载的应用的监听函数照样被调用函数内部访问DOM、组件实例、Vuex等早已销毁的对象轻则报一个TypeError重则阻断基座的状态分发流程其他正常子应用收不到状态更新。正确做法是在子应用的unmount生命周期里对称清理// 子应用代码卸载时清理监听 export async function unmount(props: any) { props.offGlobalStateChange?.() // ... 其他清理逻辑 }如果封装了自己的hook也要把取消订阅函数返回出去让调用方有义务在onUnmounted里调用。这个对称性是微前端里所有资源管理的通用法则——事件、定时器、请求、观察者凡是注册过的都要有对应的注销。5.3 快照读与响应式监听的边界第三个坑是我后来做性能优化时想明白的不是所有子应用读全局状态都需要实时响应。全局状态分两类一类是当前值是谁的快照型状态比如用户信息、系统名称子应用只需要在加载时读一次之后基本不会变或者变了也无所谓。另一类是值一变我就要跟着变的响应型状态比如主题色、语言包子应用界面必须实时跟着切换。很多团队不区分这两种状态一律用响应式监听导致全局状态的每次变化所有子应用里所有相关的组件全部重新渲染一遍。子应用少时感觉不到子应用多了以后哪个子应用里稍微一改设置全站所有页面都跟着抖动那体验非常灾难。我的经验是快照型状态用getState直接读取只在关键时机如页面加载、路由切换刷新响应型状态才用onGlobalStateChange注册监听而且监听回调里要做精确的字段比对不是所有字段都触发重渲染。这个策略在共享Pinia方案里的对应做法是快照型状态直接storeToRefs拷贝一份本地副本响应型状态才在组件里实时引用store的ref。5.4 多个子应用写同一个字段状态域的约定第四个坑来自多子应用并发写的场景。假设两个子应用分别维护自己的用户资料都往user字段上写全量对象。A应用写的是{ name: 张三, avatar: xxx }B应用写的是{ id: 1, name: 张三, email: xxxx.com }两个结构不一样谁后写谁就覆盖了对方的信息字段最后基座store里存的是被截断或错位的数据。这个问题绕不开必须在架构层定一个约定。我这里做的是状态域划分全局状态按来源应用划分模块每个子应用只能写自己的命名空间。比如把featureFlags拆成featureFlags: { appA: {...}, appB: {...} }子应用A想改自己的开关只更新featureFlags.appA不允许动appB的部分。在bridge层再做一个简单的校验拦截越权写入。// 状态域划分示例 // 全局store里定义 state: () ({ featureFlags: { appA: { enableReport: true }, appB: { enableExport: false }, }, }) // 子应用A通过自己的方法写入而不是直接去改全局store深层字段 // 基座store里定义action actionForApp(appName: string) { return { setFlag(key: string, value: boolean) { // 只允许改自己模块下的字段 this.featureFlags[appName][key] value } } }约定虽小但能把多应用写冲突问题从根源上化解。如果没有这层约定后面一定会出现这个字段到底是谁改的这种需要翻代码才能回答的问题。6. 持久化与性能优化数据不丢、页面不卡6.1 登录态和用户信息的持久化策略基座store做持久化最核心的原则是关键状态双写。我前面在store代码里已经演示了token的localStorage同步这里把完整策略说清楚。我把全局状态按是否需要持久化分成三类类型典型字段持久化策略安全态token、用户ID、角色localStorage/内存双写读取时先内存后localStorage偏好态主题、语言、布局设置localStorage持久化便于下次会话恢复瞬时态当前选中的菜单、临时开关不持久化刷新即丢只要严格按这张表落地子应用刷新后就不会有登录态丢失的幻觉问题。有一个细节需要注意子应用自身的刷新和整个页面刷新是两回事。qiankun模式下如果刷新的是子应用所在的整页基座会重新拉起来基座store会从localStorage恢复然后通过props或globalState把用户信息再次推给子应用。所以子应用里不要在mounted时直接读localStorage而要等基座把恢复好的状态推下来。我在子应用入口处对这种初始化等待做了统一处理避免每个子应用各自去localStorage里翻一遍导致数据源不统一。6.2 store瘦身与字段级监听前面原则部分已经说了宁小勿大实际操作时还有一个更细的性能优化点全局store里的大对象要拆分监听要收敛到字段级。举个例子你定义了一个全局user对象里面有profile姓名、头像、permissions几百个权限码的数组、organizations组织架构树。如果子应用监听的是整个user的变化那么权限码数组里任何一次增删都会引发全站范围的响应式更新即使跟界面上显示的姓名头像毫无关系。我的做法是拆storeuseUserStore只管用户基本信息和登录态usePermissionStore单独管权限码useOrgStore管组织架构。子应用按需引入谁的字段变化就只通知真正关心它的组件。拆完之后状态分散了但每个store的定位更清晰了变更粒度也更细了。6.3 批量更新减少跨应用通信次数最后一个性能问题来自高频状态更新。我们有个子应用是数据大屏每隔几秒会往全局store里写一段实时指标数据其他子应用要显示这个指标。如果每来一次数据就立刻setGlobalState一次通信频繁不说大屏的渲染帧率还会被qiankun的派发机制拖累。我处理的办法是批量合并高频更新进入一个缓冲区利用requestAnimationFrame或setTimeout攒一批在下一次渲染帧里统一写入。// 批量合并示例短时间内多次update只触发一次同步 let pending false function batchUpdateGlobal(callback: () void) { if (pending) return pending true queueMicrotask(() { pending false callback() // 这里统一触发 syncGlobalToChildren() syncGlobalToChildren() }) } // 使用方式数据进来只改store不马上通知 function onMetricsChange(metrics: unknown) { batchUpdateGlobal(() { store.metrics metrics }) }这样设计后无论1秒内来了多少次指标更新真正发给子应用的通知只有一次。做性能优化时记住一个公式跨应用通信的耗时 通信次数 × 单次同步成本。我们没法保证单次同步成本非常低但把通信次数降下来是可控的。6.4 状态回滚与异常兜底补充细节多写一个我在线上事故里总结出来的细节。有一次某子应用写了一个非法结构进全局store导致基座某个解析逻辑直接抛异常所有子应用的状态同步全部卡死。从那以后我要求store每个写入入口都必须做结构校验格式不对直接拒绝写入并在日志里告警。// 基座store里的一组校验简化示例 function validateUser(user: unknown): user is UserProfile { return !!user typeof user object id in user name in user } async function setUser(input: unknown) { if (!validateUser(input)) { console.error([base-store] invalid user payload, rejected, input) return } this.user input }微前端基座store是所有子应用公共依赖的地基地基的健壮性直接决定整个体系的上限。宁可让单个子应用的功能报错也不能让它污染全局状态带崩所有应用。7. 从基座store到中台化动态注册与状态可视化7.1 动态注册的模块store基座store做到一定程度后我开始考虑一个问题是不是每次新增子应用都要在基座源码里加一个对应的store模块、再发一版基座如果子应用多了基座会变成一个越来越大的公共状态垃圾桶。更好的做法是动态注册。子应用加载时通过qiankun的props回调向基座注册自己的全局store模块子应用卸载时注销这个模块。基座只保留最核心的通用store用户、主题、语言业务相关的扩展状态由子应用自己以模块形式动态挂载。// 基座侧提供注册机制示例 const dynamicModules new Map() function registerGlobalModule(moduleName: string, moduleDefinition: () StoreDefinition) { dynamicModules.set(moduleName, moduleDefinition) } function unregisterGlobalModule(moduleName: string) { dynamicModules.delete(moduleName) } // 基座在子应用mounted时如果props里带了registerModule就用它注册 props: { registerGlobalModule, unregisterGlobalModule, }子应用侧调用// 子应用代码 export async function mount(props: any) { props.registerGlobalModule?.(appA-report, () useReportStore()) } export async function unmount(props: any) { props.unregisterGlobalModule?.(appA-report) }这套机制把基座store从写死变成了扩展式基座主体保持轻量子应用的全局状态由子应用自己在运行时注入归属关系非常清楚。7.2 全局状态的跨应用可视化调试另外一个让我少掉很多头发的实践是给基座store加了可视化调试面板。微前端跨应用状态下一个问题可能是基座引起的也可能是某个子应用写脏了数据定位起来非常费劲。我们在基座里做了一个状态隧道把globalState的每次读写操作都记录到一个devLog数组里保留最近200条谁在什么时间改了哪个字段、从什么值改成什么值、来源是基座还是哪个子应用。接上这个调试面板后所有为什么状态不对的问题只需要打开面板看一眼时间线基本就能定位到凶手。我还做了全局状态快照导出/导入。线上出问题时可以把某个用户当时的全局状态快照导出来本地插入这个快照复现问题。这套东西虽然代码量不大但确实把微前端的排障难度从大海捞针降到了按图索骥。8. 写到最后聊几句我对基座store的定位基座store做了小半年我最深的一个体会是它不是一个技术名词而是一组边界约定的产物。你约定哪些状态属于全局哪些状态属于子应用你约定子应用怎么读、怎么写、怎么监听你约定谁来清理、谁来兜底。技术手段反而排在第二位——globalState、EventBus、共享Pinia实例都只是工具真正决定系统稳不稳的是你有没有把边界画清楚。如果你正准备在自己的微前端项目里搭基座store我的建议是从最小的场景起步先只放一个用户信息和一套主题切换跑通基座到子应用的同步链路再逐步按照状态模型扩充。别一上来就想着把所有状态都收进基座那和没有微前端有什么区别呢状态收得越紧子应用就越是寸步难行。最后分享一个实践小技巧无论你选了哪套方案都可以在基座store里加一个简单的版本号字段。每次store结构有破坏性变更时子应用启动后可以先校验版本号版本不匹配就提示子应用与基座版本不兼容请升级基座或子应用。这个字段平时不起眼但在多团队协作、基座独立发版的场景下能帮你省掉大量为什么这个子应用状态不对的排查时间。
返回列表