共计 3537 个字符,预计需要花费 9 分钟才能阅读完成。
提醒:本文最后更新于2025-07-07 14:38,文中所关联的信息可能已发生改变,请知悉!
electron 中的应用实例多开,会因为 session 共享而存在 indexedDB 多次打开异常。大致会遇到类似如下报错:
DOMException: Internal error opening backing store for indexedDB.open.
当应用中使用了 indexedDB,这是个必然会面对和需要解决的问题。
- 如果没有必要,可以在启动时检测和禁止应用多开。
- 若确有应用多开需求,可通过多开检测和设置独立的 session 的方式实现资源隔离。
1. 单例模式:禁止 electron 应用多开
大多数情况下,通过创建多窗口的方式即可满足大多数应用模拟多开的需求。这也是 electron 官方推荐的方式,并且给出了保持应用单例模式的方案实例。示例:
improt { app } from 'electron'; | |
let myWindow = null; | |
// 请求获取实例锁,若成功则返回 true,否则表示已存在打开的应用实例 | |
const gotTheLock = app.requestSingleInstanceLock(); | |
if (!gotTheLock) { | |
app.quit(); | |
} else { | |
app.on('second-instance', (event, commandLine, workingDirectory) => { | |
// 当运行第二个实例时退出它,并聚焦到 myWindow 窗口 | |
if (myWindow) { | |
if (myWindow.isMinimized()) myWindow.restore(); | |
myWindow.focus(); | |
} | |
}) | |
app.whenReady().then(() => { | |
myWindow = createWindow() | |
}) | |
} |
相关 API 有:
- 获取单例锁:app.requestSingleInstanceLock()
- 是否拥有锁:app.hasSingleInstanceLock()
- 释放锁:app.releaseSingleInstanceLock()
2. 使用独立 session 允许 electron 应用多开
在我们的实际应用中,允许用户开启多个客户端登陆不同的账户,indexedDB 用于缓存大量的基础数据以降低内存占用减轻 GC 压力。
这种情况下缓存数据持久化不是必须的。需本地持久化的数据并不会太多,可以通过为不同用户创建不同的本地文件加密存储方式实现。
electron 在创建 BrowserWindow 时,可通过设置 session 或 partition 参数以使用不同的 session。session 的优先级高于 partition,但 partition 指定字符串的方式更为简洁。当 partition 的值以 persist:
开头时,创建的 session 会持久化存储在 app.getPath('userData')/Partitions
目录下,否则则只创建于内存当中。
需要注意的一点是,partition 相同的 BrowserWindow 才可以共享 session(各种存储、MessageChannel 等才可以互相共享和通信),所以在创建多个不同的窗口时,若相互之间需要 session 共享,则需要设置为相同的参数值。
于是我们可以通过如下方式实现 electron 应用多开的基本方案:
- 检测是否存在应用多开
- 如果为多开,则创建窗体时,设置不同的
partition
值,以使用独立的 session
2.1 检测应用是否多开检测的方法
在 windows 下使用 wmic
命令、Linux 与 Mac OS 下使用 ps
命令,可以获取系统中所有运行应用信息。通过应用进程的 pid 与 ppid 信息可以识别是否存在应用多开。示例:
/** | |
* 获取应用全部的 pid 与 ppid 与 pid 对应列表 | |
*/ | |
function getAppPids(appExecName?: string) { | |
const pidToPpid = {} as { [pid: number]: number }; | |
try { | |
if (!appExecName) appExecName = path.basename(process.execPath); | |
const isWin = process.platform === 'win32'; | |
const cmd = isWin ? `wmic process where name="${appExecName}" GET processId,parentProcessId` : `ps -ef`; | |
const str = execSync(cmd, { encoding: 'utf8', maxBuffer: 10 * 1024 * 1024 }).trim(); | |
if (isWin) { | |
str.split('\n') | |
.map(line => line.trim().split(/\s+/)) | |
.slice(1) | |
.forEach(line => (pidToPpid[+line[1]] = +line[0])); | |
} else { | |
str.split('\n') | |
.filter(line => line.includes(appExecName)) | |
.map(line => line.trim().split(/\s+/)) | |
.forEach(line => (pidToPpid[+line[1]] = +line[0])); | |
} | |
} catch (err) { | |
logger.info('[process][getAppPids][error]', appExecName, err.message); | |
} | |
return pidToPpid; | |
}, | |
/** | |
* 当前是否为应用多开 | |
* | |
* @param {boolean} [isPrintDetail=true] 是否打印所有进程详情 | |
*/ | |
function isMultiOpen() { | |
const appName = path.basename(process.execPath).toLowerCase(); | |
const pids = this.getAppPids(appName); | |
const app = electron.app || electron.remote.app; | |
const appMetrics = app.getAppMetrics().map(d => [d.pid, d.type] as const); | |
const isMultiOpen = Object.keys(pids).length > appMetrics.length; | |
const info = { appName, isMultiOpen, pids, appMetrics }; | |
logger.info('[process]isMultiOpen', info); | |
return info; | |
} |
由于 electron
本身提供了 app.requestSingleInstanceLock
API,也可以简单的使用它来判别本次启动是否为多开:
function isMultiOpen() { | |
return !app.requestSingleInstanceLock(); | |
} |
2.2 为多开应用设置独立的 session
如果存于 indexedDB 中的数据需要持久化以便优化二次启动数据加载性能,可以为每个实例指定独立的持久化 session,否则可以创建为内存中的临时 session。主要逻辑示例:
const options: Electron.BrowserWindowConstructorOptions = { | |
useContentSize: true, | |
webPreferences: { | |
nodeIntegration: true, | |
devTools: IS_DEV, | |
nodeIntegrationInWorker: true, | |
webviewTag: true, | |
}, | |
}; | |
/** 获取可复用的 partition ID */ | |
function getPersisitId() {} | |
if (isMultiOpen) { | |
// session 持久化,主要注意可复用逻辑、避免随机创建过多持久化数据导致磁盘占用膨胀 | |
opts.webPreferences.partition = `persist:part${getPersisitId()}`; | |
// 创建为内存中的临时 session | |
// opts.webPreferences.partition = `persist_${process.pid}`; | |
} | |
const mainWindow = new BrowserWindow(options); |
注意创建多个 BrowserWindow 时 partition 参数值应相同。
3. 相关参考
- https://github.com/electron/electron/issues/10792
- https://www.electronjs.org/zh/docs/latest/api/browser-window#new-browserwindowoptions