Electron 工作区恢复要验证完整恢复链路

太阳作者太阳
原创内容采用 CC-4.0 协议发布,转载请注明出处
ElectronlocalStorage工作区排错

桌面端需要在用户选择工作区后,重启应用时自动恢复上次打开的工作区。这个需求表面上是“存在哪里”,实际要验证的是完整链路:选择工作区后是否写入、下次启动时是否读到、读到后是否真正进入页面状态。

问题背景

现象是用户选择工作区、关闭应用、重新打开后,页面仍然显示“未选择工作区”。这类问题首先要确认三件事:

  1. 当前运行的调试进程是不是使用同一个 Electron userData 目录。
  2. 渲染进程加载的是不是最新的 Vite 模块。
  3. 代码到底把“上次工作区”写到了哪里。

一次实际排查中,运行进程使用的是下面这类目录:

C:\Users\WINDOWS_USER\AppData\Roaming\DESKTOP_APP

开发服务器也能返回最新的目标页面模块。这说明问题不能简单归因于“跑的是旧应用”;但这类证据只能排除一部分可能性,不能证明持久化和恢复链路已经执行。

根因

这次最初的结论写得不准确:问题不能归结为“没有写 localStorage”。如果在 Electron userData 目录写入一个 JSON,并且启动时能通过 IPC 读回,再把读到的路径应用到 workflowState.workspace,功能同样应该成立。

真正的问题是当时没有完成端到端证据验证。我把“应该使用 localStorage”和“JSON 方案为什么没生效”混成了一个结论。正确的排错应该把链路拆开验证:

  1. 选择工作区后,偏好文件或 localStorage 是否真实写入。
  2. 重启后,恢复逻辑是否真实执行。
  3. 恢复逻辑读到的路径是什么。
  4. 恢复函数是否成功加载工作区状态并更新页面状态。
  5. 如果失败,错误是否展示在“未选择工作区”页面,而不是只写进日志。

当时错误实现创建了:

workspace-preferences.json

这个 JSON 方案本身不是不能工作。它的问题在于实现增加了主进程 IPC、preload 暴露、授权检查、异步恢复、页面状态应用多个环节,但当时只验证了工具函数测试通过,没有用运行时证据证明每一层都通。用户看到重启后仍是“未选择工作区”,说明断点一定发生在“读取 JSON 后应用到页面状态”这条链路上,而不是“存 JSON 这种方式天然不行”。

最终采用 localStorage,是因为这类非敏感、仅供当前页面恢复使用的偏好可以直接在渲染进程内读写,从而缩短链路:

const lastWorkspaceDirStorageKey = "desktop-app.last-workspace-dir.v1";

function loadLocalLastWorkspaceDir(): string | null {
  if (typeof window === "undefined") return null;
  return window.localStorage.getItem(lastWorkspaceDirStorageKey);
}

function saveLocalLastWorkspaceDir(workspaceDir: string): void {
  if (typeof window === "undefined") return;
  window.localStorage.setItem(lastWorkspaceDirStorageKey, workspaceDir);
}

Electron 会通过 Chromium session 持久化 localStorage。其 sessionData 路径默认与 userData 相同,磁盘上通常能看到类似下面的目录:

C:\Users\WINDOWS_USER\AppData\Roaming\DESKTOP_APP\Local Storage\leveldb

Local Storage\leveldb 是 Chromium 当前的内部存储布局,不是应用可以依赖的稳定 API。应用应通过 window.localStorage 访问数据,通过 app.getPath("userData")app.getPath("sessionData") 确认运行时路径,不应直接读写 LevelDB 文件。

选择 localStorage 后不再需要额外的 workspace-preferences.json。如果以后重新采用 JSON 方案,必须用运行时验证或测试覆盖完整链路,而不是只测文件读写函数。

不要把 profile 目录结构当成应用接口

Electron 的 userData 适合存放应用配置,sessionData 存放 Chromium session 产生的 localStorage、cookie、磁盘缓存和网络状态。sessionData 默认指向 userData,但应用可以在 ready 事件前覆盖它,因此两者不能永远画等号。

排查时可以观察 profile 目录辅助判断,但其中大部分子目录属于 Chromium 实现细节,名称和布局可能随版本变化。常见内容包括:

  • Local Storage:Chromium Web Storage 的内部持久化数据,不应由应用直接修改。
  • Session Storage:渲染进程 sessionStorage,适合临时状态,不适合跨重启恢复工作区。
  • APP_DATA_SUBDIR:应用自行管理的配置或业务状态,应使用明确的子目录和文件名。
  • CacheCode CacheGPUCacheDawnGraphiteCacheDawnWebGPUCache:Chromium 资源、JS/WASM 编译、GPU/WebGPU 缓存,可再生成。
  • Network:Chromium 网络 profile 数据,包括 cookies、trust tokens 和网络持久状态。
  • blob_storage:浏览器 Blob API 的存储目录。
  • Dictionaries:Chromium 拼写检查词典。
  • Shared DictionarySharedStorage:Chromium 的共享字典压缩缓存和 Shared Storage 数据库。
  • Local StatePreferences 等:Chromium/Electron profile 级别状态和偏好,不是应用手写的业务配置。

还要验证 origin 和 session partition

即使 profile 路径相同,localStorage 也不会自动跨 origin 或 session partition 共享。开发环境从 http://localhost:PORT 加载、生产环境从自定义协议或本地文件加载时,可能对应不同的存储空间;使用 persist:NAME partition 的窗口,也可能与默认 session 分离。

因此完整排查至少要记录:

  1. 当前页面的 location.origin
  2. BrowserWindow 使用的 partition,是否带有 persist: 前缀。
  3. main 进程返回的 app.getPath("userData")app.getPath("sessionData")
  4. 写入和恢复发生时使用的 storage key 与实际值。

经验

遇到 Electron “重启后没有恢复”的问题,不能只争论存储介质。无论使用 localStorage、JSON 文件还是数据库,都要验证同一条链路:写入成功、下次启动读取成功、读取结果进入 React 状态、页面没有被空状态提前挡住、失败原因能显示出来。

如果需求明确是 localStorage,可以让渲染进程直接读写 window.localStorage,并把它用于非敏感、与当前页面 origin 和 session 绑定的偏好。授权 token、原生能力配置、需要主进程权限或加密保护的数据,则更适合由主进程管理。工作区路径本身也可能暴露用户名、客户名或项目名,不应把 localStorage 当成秘密存储。

验证时不要只看源码,还要确认:

Get-ChildItem -Force "$env:APPDATA\DESKTOP_APP"

同时应从 main 进程打印稳定 API 返回的实际路径:

console.log({
  sessionData: app.getPath("sessionData"),
  userData: app.getPath("userData"),
});

如果启动参数或测试工具覆盖了 Chromium profile,也要核对实际使用的 user data directory。应用自身需要定制目录时,优先在 ready 之前使用 app.setPath(),不要把内部子目录结构写死在业务逻辑中。这样才能区分“代码没生效”“origin 或 partition 变化”“跑错 profile”和“存储位置理解错误”几类问题。

参考资料