Electron 主进程读配置文件,难点不在 fs.readFile,而在配置失效时如何不拖垮主进程。先判断风险:配置文件路径是否固定、文件是否可能被外部编辑器改写、更新后是否要让渲染进程立刻感知。这三个问题没想清楚,直接写监听代码会留下一堆边界坑。
先把路径和默认值定下来
在主进程中读取本地配置文件,首先需要确定文件存放的位置。对于不同操作系统,建议使用 app.getPath('userData') 作为基础目录,再拼接文件名,例如 path.join(app.getPath('userData'), 'config.json')。这样既能保证用户有读写权限,也能避免将配置写在安装目录下因权限不足而失败。判断条件可以这样写:如果 fs.existsSync(configPath) 为 false,则回退到默认配置对象,避免程序启动即崩溃。另外,使用 JSON.parse 读取时要用 try/catch 包裹,一旦文件内容被用户编辑成非法 JSON,至少能捕获错误并提示日志,而不是让主进程直接退出。
这段逻辑适合大多数桌面端场景。userData 目录在 Windows 下对应 AppData\Roaming,macOS 下对应 Library/Application Support,不需要自己拼 home 路径。启动时先做存在性检查,能区分首次运行和配置损坏。另外 try/catch 里除了 JSON.parse,还要检查解析结果是不是普通对象,比如解析出数组或 null 也要回退到默认配置。
const fs = require('fs');
const path = require('path');
const configPath = path.join(app.getPath('userData'), 'config.json');
function loadConfig() {
if (!fs.existsSync(configPath)) return defaultConfig;
try {
const raw = fs.readFileSync(configPath, 'utf8');
const parsed = JSON.parse(raw);
return parsed && typeof parsed === 'object' ? parsed : defaultConfig;
} catch (err) {
console.error('config parse error:', err.message);
return defaultConfig;
}
}代码里的 defaultConfig 必须是完全独立的常量对象,不能被后续修改污染。如果多个模块共享同一个引用,一旦某个地方改了配置,默认值也会跟着变。
监听文件变化前,先给回调加上防抖
当用户在编辑器中连续保存文件时,fs.watchFile 可能会在短时间内触发多次回调,每次回调都去更新配置会造成不必要的开销。建议在监听回调中加入防抖逻辑:用一个定时器延迟 200 毫秒执行真正的读取和更新,如果期间又有新的变更则重置定时器。判断条件是,若定时器已经存在,就 clearTimeout 再重新 setTimer。这样可以确保只在用户停止编辑后才生效一次。另外一个常见坑是,如果配置文件通过重命名替换的方式更新(例如写临时文件再 rename),监听可能会失效,因为 inode 变化。遇到这种情况,可以在 watchFile 的 callback 里重新思考,结合 stat 的 ino 字段判断,或者改用 chokidar 这类库来监听整个目录。
fs.watchFile 是轮询机制,默认每 5 秒检查一次,interval 参数虽然能调短,但间隔太短会放大磁盘 I/O。防抖是第一步,但不要只做防抖。真正读取文件时,还要判断这次读取到的 JSON 是否完整,因为编辑器保存时可能先截断再写入,读到空字符串或半个对象很常见。素材里提到的 inode 变化问题,在 Windows 和 macOS 上也会遇到,如果你改完配置后监听不触发,优先怀疑是不是程序或编辑器用 rename 替换了原文件。
新配置推给各窗口,别直接改全局对象就算了
动态修改的核心是如何把新配置推送到业务模块。一种方式是直接修改全局配置对象,然后在主进程内触发事件,通知各窗口通过 IPC 重新拉取配置。具体动作是:在监听回调里调用 configManager.set(newConfig) 更新内存,再对所有 BrowserWindow 发送 webContents.send('config-updated', patch) 消息。为了避免频繁刷屏,可以加一个条件:只有当配置内容与上次不同时才发送。如果配置改动涉及窗口尺寸、主题等界面属性,还需要在渲染进程中监听该消息并执行相应动作。注意,配置文件可能是由一个编辑器或者脚本整体重写,所以监听后不要假设变更一定是增量,最好全量对比。
这段的要点是“全量对比”。配置来自外部编辑时,很难判断用户改了哪个键,不如直接替换整个内存对象。发送 IPC 消息时最好带上完整的新配置,或者明确的 patch 结构,渲染进程自己决定哪些字段要应用。对于没有监听消息的窗口,可以在窗口创建时主动拉取一次当前配置,保证新开窗口也能拿到最新值。
字段校验要具体到类型和范围
配置里用户手动填写的字段,不能直接拿来用。端口就用 Number.isInteger 检查,字符串枚举就检查是否在允许列表内,路径要确认落在 userData 或应用允许的目录下。校验不通过时,保留旧值并输出警告,而不是接受非法值。边界是:用户改坏配置文件时,程序不会崩溃,但改动不生效,日志里要指明哪个字段出了问题。更严格的做法是引入 JSON Schema,但会增加依赖;项目不大时,手写校验函数反而更灵活。另一点不要做:校验通过后把配置写回文件,否则会覆盖用户手写的注释或格式,下次监听还会触发一次无意义的更新。
改完看这几个信号,再决定要不要回滚
配置动态修改是否生效,至少看三个信号:主进程日志里有没有输出 config-updated 相关记录;目标窗口的界面属性有没有变化;再次手动编辑同一字段,配置是否还能继续更新。如果监听回调触发了但界面没变,先确认渲染进程真的监听了 ipcRenderer.on,还是只监听了一次就失效。如果配置文件被改坏,程序应该保留上一次可用配置,同时在日志里输出具体字段名,而不是直接崩溃。
如果程序需要主动写回配置,写入前先创建 config.json.bak,再通过临时文件 rename 覆盖原文件。写入完成后立刻读回来校验,失败就从备份恢复。这个边界在用户手动编辑和程序写回并存时尤其重要,不然两边互相覆盖,问题会很难查。读取时遇到 EACCES 权限错误,不要静默忽略,要在主进程日志里记录,并在界面提示用户检查目录权限。