Electron 应用多开 indexedDB 打开异常问题分析与解决

目录
[隐藏]

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 有:

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
点赞 (2)
  1. gavin说道:
    Google Chrome 119.0.0.0 Google Chrome 119.0.0.0 Windows 10 x64 Edition Windows 10 x64 Edition

    你好 获取可复用的 partition ID 这个有什么具体思路吗 假如我想根据登陆后的用户id设置partition 大拿不吝赐教!!

    1. 任侠说道:
      Microsoft Edge 120.0.0.0 Microsoft Edge 120.0.0.0 Windows 10 x64 Edition Windows 10 x64 Edition

      文章中的示例代码基本包含了全部主要内容,具体怎么设计取决于你项目具体的需求了
      如果你想创建系统级的分离,可以在拿到用户 id 后为其创建独立的窗体,窗体创建过程中按用户 id 设置 partition id 的取值即可

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注

Captcha Code