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
你好 获取可复用的 partition ID 这个有什么具体思路吗 假如我想根据登陆后的用户id设置partition 大拿不吝赐教!!
文章中的示例代码基本包含了全部主要内容,具体怎么设计取决于你项目具体的需求了
如果你想创建系统级的分离,可以在拿到用户 id 后为其创建独立的窗体,窗体创建过程中按用户 id 设置 partition id 的取值即可