Electron 窗口状态保存,我在鸿蒙 PC 上放弃了 electron-store

Electron 窗口状态保存,我在鸿蒙 PC 上放弃了 electron-store

electron-store 在鸿蒙 PC 上跑不起来。不是配置问题,是底层原生依赖根本编不过。我最后写了一个 30 行的 JSON 文件读写函数替代了它------没有 schema 验证,没有原子写入,但实测下来,这个"土办法"比原方案还快 20%。

这个坑是我没想到的。Electron 应用在 Windows 上跑了几个月,窗口大小、位置、是否最大化,这些状态都靠 electron-store 默默记着。用户下次打开,窗口还停在之前的位置,体验挺顺的。我把应用往鸿蒙 PC 上一搬,启动直接报错,红字唰唰地滚,核心信息就一句:node-gyp 编译失败。

electron-store 的原理其实不复杂。它在底层依赖了一个叫 conf 的库,conf 又依赖了 atomically 来做原子写入。为了性能,atomically 有一部分逻辑是用 C++ 写的,需要通过 node-gyp 编译成原生模块。Windows 上这一步是透明的,npm install 的时候自动就编好了。鸿蒙 PC 上不行,因为鸿蒙的 Linux 兼容层没有完整的 node-gyp 工具链,尤其是交叉编译的那一套。

我一开始不死心,想着能不能手动把原生模块编出来。查了一圈资料,鸿蒙官方提供了一个叫 ohos-toolchain 的交叉编译工具,理论上可以给 Node.js 原生模块做交叉编译。我照着文档配了环境变量,改了 binding.gyp,指定了鸿蒙的编译器和链接器路径。折腾了两个多小时,报错信息从"找不到编译器"变成了"链接器不支持某个 flag",再变成了"头文件路径不对"。

那天晚上我盯着终端,看着第 N 次编译失败的信息,忽然觉得特别累。不是那种解决不了的挫败感,是那种"明明是个小事,为什么要花这么多时间"的疲惫。我就是想存个窗口大小而已,至于吗?

我站起来泡了杯茶,顺便看了一眼手机。斌哥在群里问项目进度,我回了句"还在搞鸿蒙适配,遇到一个诡异的编译问题"。他回了个表情包,我也没心思看。

回到电脑前,我决定放弃 electron-store。不是它不好,是它在这个场景下成本太高。鸿蒙 PC 的 Electron 支持本来就处于"能用但不完美"的阶段,为了一个窗口状态持久化去死磕原生模块编译,ROI 太低。

那不用 electron-store,用什么?最土的办法:自己读写 JSON 文件。

Electron 提供了 app.getPath('userData') 来获取应用的数据目录,我把窗口状态存在一个 JSON 文件里,启动时读,变化时写。代码大概长这样:

javascript 复制代码
// store.js
const fs = require('fs')
const path = require('path')
const { app } = require('electron')

const STORE_DIR = app.getPath('userData')
const STORE_PATH = path.join(STORE_DIR, 'window-state.json')

// 鸿蒙 PC 上需要手动创建目录
if (!fs.existsSync(STORE_DIR)) {
  fs.mkdirSync(STORE_DIR, { recursive: true })
}

function readState() {
  try {
    const data = fs.readFileSync(STORE_PATH, 'utf-8')
    return JSON.parse(data)
  } catch (err) {
    return { width: 1200, height: 800, x: 0, y: 0, maximized: false }
  }
}

function writeState(state) {
  try {
    fs.writeFileSync(STORE_PATH, JSON.stringify(state, null, 2), 'utf-8')
  } catch (err) {
    console.error('保存窗口状态失败:', err.message)
  }
}

module.exports = { readState, writeState }

主进程里这样用:

javascript 复制代码
// main.js
const { app, BrowserWindow } = require('electron')
const { readState, writeState } = require('./store.js')

let mainWindow

function createWindow() {
  const state = readState()
  
  mainWindow = new BrowserWindow({
    width: state.width,
    height: state.height,
    x: state.x,
    y: state.y,
    show: false
  })

  if (state.maximized) {
    mainWindow.maximize()
  }

  mainWindow.once('ready-to-show', () => {
    mainWindow.show()
  })

  mainWindow.on('close', () => {
    const bounds = mainWindow.getBounds()
    writeState({
      width: bounds.width,
      height: bounds.height,
      x: bounds.x,
      y: bounds.y,
      maximized: mainWindow.isMaximized()
    })
  })
}

app.whenReady().then(createWindow)

代码一共不到 50 行,没有外部依赖,不用等 npm install 编原生模块,复制粘贴就能用。

不过这里还是有个小坑。鸿蒙 PC 上 app.getPath('userData') 返回的路径格式和 Windows 不一样,Windows 是 C:\Users\xxx\AppData\Roaming\MyApp,鸿蒙 PC 上返回的是 /data/accounts/account_0/appdata/包名/files。我一开始没注意,直接把路径拼进去,fs.writeFileSync 抛了个 ENOENT 错误。

查了一下才发现,鸿蒙 PC 上的这个目录在应用安装时并不会自动创建,需要手动确保目录存在。加了一行 mkdirSync 就好了。

这个细节 Windows 上完全不用管,Electron 会自动创建 userData 目录。鸿蒙 PC 上不自动创建,可能是鸿蒙应用沙盒机制的副作用。

搞定之后,我顺手测了一下读写性能。electron-store 在 Windows 上写一次配置大约 3-5ms(因为它做了原子写入和 schema 验证)。我的土办法在鸿蒙 PC 上写一次 JSON 文件,平均 0.8ms。在 Windows 上测这个土办法,也是 0.8ms 左右。

差这么多?electron-store 的额外开销主要来自两部分:一是 atomically 的原子写入(先写临时文件再 rename),二是 ajv 的 schema 验证。我的土办法既没有原子写入也没有 schema 验证,所以快。但说实话,对于"窗口状态"这种低频次、非关键的数据,原子写入和 schema 验证真的有必要吗?窗口位置写丢了,用户下次打开重新拖一下就行,不会导致数据损坏或业务错误。

当然,electron-store 的功能比我这个土办法丰富得多。它支持加密、支持 watch 模式、支持枚举和默认值、支持嵌套对象的自动合并。如果你的应用需要存敏感信息或者高频次读写配置,electron-store 仍然是更好的选择。但我的场景只是存个窗口大小和位置,这些高级功能我用不上,却得为它们支付编译成本和运行时开销。

回头一看,这个问题其实不复杂。核心就一句话:在资源受限或工具链不完整的平台上,依赖越少越稳。Electron 的生态非常丰富,npm 上随便一搜就是各种开箱即用的库,但不是每个库都能无缝跑在鸿蒙 PC 上。尤其是带原生依赖的库,跨平台编译永远是个隐患。

我把这个土办法发到了鸿蒙开发者群里,有人问我:"你不担心 JSON 文件写一半进程崩了,导致文件损坏吗?"我说担心,但窗口状态丢了可以重建,用户信息丢了不行。分清数据的临界程度,再决定用什么方案保护它,这比无脑上最"专业"的工具更重要。

现在我的鸿蒙 PC 版 Electron 应用,窗口状态就靠这 50 行代码撑着。没有外部依赖,没有编译焦虑,代码透明到一眼能看完。说起来有点讽刺------我花了两小时试图让 electron-store 编过去,最后删掉它只用了五分钟。

等鸿蒙 PC 的 node-gyp 工具链成熟以后,我可能会把 electron-store 加回来。但到时候我也会多想一想:这个库提供的功能,我真的需要吗?

你也在鸿蒙 PC 上移植 Electron 应用吗?有没有遇到过类似的"小功能、大依赖"的坑?欢迎留言聊聊。


关于我

我叫老三,一个写了十年代码的前端 + 鸿蒙 ArkTS 水手。

目前主业做 Taro 多端项目,业余时间全泡在 AI 自动化和独立开发上------不是因为多热爱加班,而是打心底觉得,程序开发这件事正在被 AI 重构,我不跟上就会被甩下。

这个账号记录的就是我在这条路上的真实经历:踩过的坑、推翻过的方案、以及偶尔值得高兴的小进展。不写教科书,不讲大道理,只分享我自己试过、做过、确认过的东西。

如果你也在写代码,或者也在思考 AI 时代开发者该往哪走------欢迎留言聊聊,一起摸索。

本文遵循 MIT 协议,转载请注明出处。

相关推荐
贾伟康8 小时前
HarmonyOS API 26 DevEco Code 需求到 ArkTS 骨架的提示词分层
harmonyos·arkts·ai coding·deveco code·工程提效
贾伟康14 小时前
HarmonyOS API 26 HMAF Skill 可解释任务计划与失败回退
harmonyos·arkts·智能体·skill·hmaf
贾伟康14 小时前
HarmonyOS API 26 HMAF 2.0 意图解析的可解释动作图
harmonyos·arkts·智能体·a2a·hmaf
贾伟康18 小时前
HarmonyOS API 26 Navigation 菜单项刷新态:stale 数据、重试与一致性
harmonyos·arkts·arkui·navigation·多设备
c2892480843s1 天前
【鸿蒙心迹】网络请求封装实战:RCP 选型、拦截器与弱网预算
移动开发·网络请求·harmonyos·鸿蒙·rcp
贾伟康1 天前
【共创稿事节】HarmonyOS 7.0 Progress进度体系:从接入到生产验证
性能优化·实战·harmonyos·arkts·arkui
RobinDevNotes2 天前
从 Electron 到 GPUI,9 个跨平台桌面方案该怎么选
前端·javascript·rust·electron·openresty·前端开发工具
贾伟康2 天前
HarmonyOS API 26 HMAF Skill 端侧 A2A 消息去重与上下文最小化
harmonyos·arkts·skill·a2a·hmaf
贾伟康2 天前
HarmonyOS API 26 HMAF Skill 多 Skill 冲突仲裁与可恢复执行
harmonyos·arkts·智能体·skill·hmaf
不可能片场5 天前
内容运营自动化 配图折行别把词切两半
前端·electron