Garfish 主子应用如何通信?状态共享有哪些边界和坑?
Garfish 主子应用通信不要一开始就设计成“大而全的全局 store”。更稳的做法是按数据生命周期拆分:挂载时需要的上下文用 props,跨应用通知用事件总线,长期共享的登录态、主题、权限用平台服务封装。这样子应用仍能独立开发和部署,主应用也不会变成所有业务状态的垃圾桶。
props 传稳定上下文
props 适合传用户信息读取函数、API 前缀、路由 basename、权限快照和平台服务入口。它简单、可测试,子应用挂载时就能拿到,不需要等待异步事件。边界是 props 不适合高频变化数据,比如未读数、协同编辑状态或实时价格;这些数据用 props 传,会让刷新和同步逻辑很难维护。
jsGarfish.run({ apps: [{ name: 'order', entry: 'https://cdn.example.com/order/', activeWhen: '/order', props: { basename: '/order', services: { auth: { getToken: () => auth.getToken(), logout: () => auth.logout() }, theme: { get: () => theme.current } } } }] })
这里传函数比传固定 token 更安全。登录刷新后,子应用再次调用 getToken 能拿到新值,不会继续使用旧凭证。
事件总线传“发生了什么”
事件适合通知主题切换、登录过期、订单创建成功、侧边栏收起这类动作。它的优点是解耦,一对多传播很方便。踩坑点是事件不是状态存储,晚挂载的子应用可能错过历史事件,所以关键状态仍要能主动读取。
jsexport const platformBus = { emitTheme(theme) { Garfish.channel.emit('platform:theme-change', { theme }) }, onTheme(handler) { Garfish.channel.on('platform:theme-change', handler) return () => Garfish.channel.off('platform:theme-change', handler) } }
每次订阅都要在卸载时取消,否则重复进入子应用后会出现多次触发,看起来像接口重复请求,实际是监听器泄漏。
共享状态要少而稳定
真正值得共享的状态通常只有登录、权限、主题、语言、租户和少量平台配置。表格筛选、弹窗开关、详情页临时数据应留在子应用内部。共享越多,子应用越难独立运行,灰度发布和回滚也越麻烦。
通信契约最好放在单独包里维护,至少包含事件名、payload 类型和服务方法签名。这样主应用升级时,旧子应用能通过 TypeScript 或测试提前发现不兼容,而不是上线后才暴露。对于跨团队协作,契约比口头约定更可靠。如果担心类型包升级影响发布,可以让契约保持向后兼容:新增字段可以,删除字段要经过灰度期。事件名也不要随业务文案变化而变化,最好用稳定的领域语义。
追问
props、事件总线和共享服务怎么取舍?
props 用来给子应用启动参数,事件总线用来通知变化,共享服务用来读取长期状态。判断标准是数据是否需要被晚挂载应用读取;如果需要,就不能只靠事件。实践里常见组合是 props 传服务入口,事件做变更通知,服务保存当前值。
子应用之间能直接通信吗?
技术上可以,但不建议作为主链路。A 应用直接依赖 B 应用的事件名,会让两个团队发布节奏绑在一起。更好的边界是由主应用定义平台事件或领域服务,子应用只依赖稳定契约。
token 应该作为 props 直接传吗?
短期可以,但长期更推荐传 getToken 函数或认证服务。直接传字符串容易在刷新、退出登录、切换账号后变成旧值。踩坑最多的是子应用自己缓存 token,主应用已经退出,它还在继续发请求。
如何避免通信导致内存泄漏?
订阅事件、WebSocket、轮询都必须在 unmount 或组件卸载钩子里释放。不要用匿名函数订阅后无法 off,也不要在 render 中重复订阅。排查重复触发时,先看监听器数量,再看接口层。