Electron 多窗口复杂应用的状态管理与通信

简单的 Electron 应用往往只有一个窗口,状态都在同一个渲染进程里处理就够了。
一旦扩展到多窗口、多面板、甚至多个「子应用」,状态管理和进程间通信就变成了绕不开的话题。
这一篇从工程角度拆开几个问题:哪些状态应该共享、哪些状态只属于某个窗口,多窗口之间如何通信,以及如何让主进程在这一套结构里扮演合理角色。

在多窗口场景里,容易出现一种现象:
每个地方都在维护自己的状态副本,修改一处却忘记同步其他地方。

可以先按「范围」把状态分成几类:

  • 全局状态
    • 与当前用户、会话或整个应用相关,例如登录信息、全局配置、许可状态等;
    • 可能需要在多个窗口中以某种形式展示或使用。
  • 窗口级状态
    • 仅与某个窗口相关的 UI 和交互状态,例如当前选中的标签页、滚动位置、表单内容等;
    • 通常不需要跨窗口共享。
  • 持久化状态
    • 与本地存储相关,例如设置、缓存、项目索引等;
    • 生命周期超出单次运行,需要在磁盘上读写。

这三类状态的混淆,是多窗口应用混乱的常见根源。
一个更健康的结构是:先决定每一类状态归属哪一层,再讨论通信问题。

主进程本身不适合堆所有状态,但有几类信息放在这里会更自然:

  • 真正全局的状态
    • 比如当前激活的工作区、全局配置的内存镜像、某些后台任务的状态;
    • 主进程可以作为这些状态的「单一事实来源」。
  • 与系统资源紧耦合的状态
    • 比如当前打开的文件句柄、活跃的子进程、网络连接等;
    • 本身就只能在主进程或本地服务模块中管理。

渲染进程则:

  • 通过受控 API 获取全局状态的快照;
  • 通过 IPC 通知主进程修改状态或订阅状态变更。

这样可以避免:

  • 多个窗口各自维护一份全局状态副本;
  • 主进程被迫在很多地方「猜」当前状态。

窗口级状态通常由前端框架自带的状态管理方案处理:

  • 对 React 应用,可以使用 Context/Redux/Zustand 等;
  • 对 Vue 应用,可以使用 Pinia/Vuex 等。

这部分状态的特点是:

  • 与具体 UI 强相关;
  • 生命周期与窗口绑定;
  • 即便其他窗口不知道这些状态,也不会影响整体正确性。

在工程实践中:

  • 尽量避免为窗口级状态设计跨窗口同步机制;
  • 如果确实需要共享结果,可以通过「先写主进程/本地服务 + 再由其他窗口重新拉取」的方式,而不是实时镜像每个局部变化。

多窗口通信可以通过主进程路由,也可以借助外部服务。常见模式包括:

  • 主进程作为消息总线
    • 窗口 A 通过 IPC 把消息发给主进程;
    • 主进程根据逻辑决定是否把消息转发给窗口 B 或更新全局状态;
    • 窗口 B 订阅来自主进程的某类事件。
  • 基于持久化状态的间接通信
    • 窗口 A 修改某个持久化状态(例如写入配置或数据库);
    • 窗口 B 订阅这个状态的变更事件或周期性刷新;
    • 用 watch 机制代替直接一对一消息。
  • 使用外部消息总线/后端服务
    • 对于更复杂的应用,可以让所有窗口通过 WebSocket/HTTP 与后端交互;
    • 窗口之间的消息同步由后端负责,Electron 只负责本地展示和控制。

选择哪种模式,取决于应用是否有现成的后端服务,以及对实时性和复杂度的要求。

在多窗口场景下,如果 IPC 消息和事件名是到处随手拼出来的,很快会难以维护。
更稳妥的做法是把通信协议当作「本地 API 设计」来对待:

  • 为主进程提供一组清晰的服务接口:
    • 例如 config.get() / config.set()workspace.open(path)tasks.subscribeStatus(id) 等;
    • 每个接口对应明确的请求/响应数据结构。
  • 对跨窗口广播事件:
    • 用常量定义事件名,集中管理;
    • 让渲染进程订阅特定事件,而不是随意监听某个字符串。

这样可以:

  • 让多窗口通信在代码层面表现得和调用服务类似,而不是一堆「不知道谁在发、谁在收」的事件流;
  • 更容易在以后引入类型系统或协议生成工具。

可以用几个常见场景来检验设计:

  • 场景一:主窗口打开的项目在子窗口中也需要使用
    • 全局「当前项目」状态放在主进程或本地服务模块;
    • 子窗口在打开时,通过 API 拉取当前项目信息;
    • 当项目切换时,由主进程向所有相关窗口广播「项目切换」事件。
  • 场景二:子窗口中的操作需要刷新主窗口中的列表
    • 子窗口调用某个服务接口完成操作(例如新增记录);
    • 服务完成后向主窗口发送「数据变更」通知;
    • 主窗口根据通知选择刷新列表或局部更新。
  • 场景三:多个窗口共享某个后台任务的进度
    • 后台任务状态由主进程或本地服务管理;
    • 所有窗口通过订阅接口获取任务进度推送;
    • 不在每个窗口里单独拉一遍远端或本地状态。

这些场景共同强调的一点是:

  • 共享状态应该集中管理,通过明确的接口向各窗口分发,而不是在窗口之间直接互相读写。

可以用一条简短的原则结束这一篇:

  • 先从「状态归属范围」出发,明确哪些是全局、哪些是窗口级、哪些需要持久化;
  • 把全局和持久化状态集中在主进程/本地服务或后端服务中管理,通过受控接口和事件分发给各窗口;
  • 让每个渲染进程只管自己的 UI 和局部状态,把窗口之间的通信和同步交给一个设计良好的「状态与消息中枢」。

沿着这条思路搭结构,多窗口 Electron 应用的复杂度会更可控,后续扩展和重构成本也会明显降低。