electron 应用开发,在工作进程中访问主进程中变量值的实践分享
develectron-app, worker-threads

electron 应用开发,在工作进程中访问主进程中变量值的实践分享

Published On
(C) unsplash
7分钟阅读bb98dd8a
概述:在 electron 桌面应用开发中,某些场景中,可能会遇到这样情况:同一变量值、或导出模块(如类的实例)被主进程、工作进程同时调用,(BUT, 该变量值、该类实例依赖的第三方模块却不被允许在工作进程中被访问),在这种情况下可行解决方案的实践分享

背景

最近吾辈维护开发一个 electron 桌面应用,遇到了开发模式下正常,打包部署下异常情况,一番排查后,遇到报错大致如:

cannot find module "electron-store"
# or
cannot find module "electron-log/main"

这样的报错出现在,工作进程中的层级调用链中,有尝试调用 electron 生态下的 store/ log 原生功能模块,但实际上,相关模块被禁止在工作进程中访问,故而给出了报错:module cant be found.

正文

比如这样的情况,通过 "electron-store" 定义了缓存用户配置的变量 userConfig = new Store({database: ...}), 其中有关于访问本地数据库的所需的账密信息。 再有一个操作数据库的类 ClzHdlDatabase {},如果该类 ClzHdlDatabase 定义所在的源代码中,尝试直接访问 userConfig#database,

当在主进程中调用该类实例,没有问题,程序正常运行。而如果在工作进程中也因为有操作数据库的需要而调用了该类,便会出现如同上面的报错。

旧有解决方案

(如果嫌弃文本长度,可直接跳转👉🏻 文末,吾辈对旧有代码重构后的代码段)

如何解决?项目的原有代码给出了大致这样的实现,总体来说,是将 ClzHdlDatabase 定义中的 userConfig#database 抽离,以构造函数的形式传入给 ClzHdlDatabase,这样便规避了在 ClzHdlDatabase 文件本身(或调用链上)尝试直接访问 electron-store 模块的情况。

ClzHdlDatabase.js
class ClzHdlDatabase {
  constructor(databaseConfig) {
    this.dbConfig = databaseConfig;
  }
  // ...
}

在这样的定义形式下,主进程中的调用或许如

demo-in-main-threads.js
const dbConfig = getUserConfig().database;
const demoClzHdlDatabase = new ClzHdlDatabase(dbConfig);

而在工作进程中,前提是将 databaseConfig 通过进程间通信,从主进程传递给工作进程(略),则调用形式或与如:

demo-in-worker-threads.js
const {workerData} = require("node:worker_threads")

const {config} = workerData;
const {database: dbConfig} = config;

const demoClzHdlDatabase = new ClzHdlDatabase(dbConfig);

看似可行,但实际项目中往往调用栈复杂,并且可能调用链上多个文件层级都需要访问依赖(工作进程中禁用)模块定义的功能函数或变量。以下面的实际代码为例:

temperature-data-hdl.js
class TemperatureDataHandler {
  /** >. 构造函数中部分入参是用于 TemperatureService 初始化 */
  constructor(sensorsPerBlade, totalBlades, samplingRate, alertsConfig, dbConfig, loggerTool) {
    this.SENSORS_PER_BLADE = sensorsPerBlade;
    this.TOTAL_BLADES = totalBlades;
    this.DB_WRITE_INTERVAL = samplingRate * 1000;

    this.temperatureService = TemperatureService.getOnlyInstance(sensorsPerBlade, totalBlades, alertsConfig, dbConfig);

    // ...

    this.loggerTool = loggerTool;
  }

  // ...
}

在上面的构造函数处, (sensorsPerBlade, totalBlades, samplingRate, alertsConfig, dbConfig, loggerTool), 其中前 5 个是依赖 electron-store 模块定义的关联用户配置的缓存值,最后的 loggerTool 显然是依赖 electron-log 的工具函数。虽然这样的构造函数的一个优化点是精简传参形式,譬如:定义为:constructor(userConfigOptions, loggerTool) 。但仍然有痛点,调用链上对比如 userConfigOptions、loggerTool 的多层级传递。

可能读者会说,无需通过构造函数层级传递,也无需将这些配置值访问定义在构造函数形参处,完全可以在 ClzHdlDatabase 文件头部通过: const {workerData} = require("node:worker_threads") 直接拿取主进程传递过来的值。但是我们面对这样的情况, ClzHdlDatabase 会分别被主进程、工作进程所调用。(项目实际场景比如:单开工作进程中调用该工具类来高频的写数据库,而主进程调用该工具类来读数据库)

📌 重构优化后的解决方案

jump-here

不再冗余陈述,我们可以依赖 api#isMainThread 和 NodeJS 的条件式引入模块的特性,来定义如下的工具函数,以让 ClzHdlDatabase 能被(主进程、工作进程)兼容调用,且无需将相关变量隔离到构造函数形参处。

getStoreConfigAlso4Worker.js
const { isMainThread, workerData } = require("node:worker_threads");

const storeConfig = isMainThread ? require("~/path/to/store-config") : null;

/**
 * 同时兼容在主进程、单开工作进程中调用
 */
function getStoreConfigAlso4Worker() {
  if (isMainThread) {
    return storeConfig.getConfig();
  }
  const {
    config,
  } = workerData;

  return config;
}

module.exports = {
  getStoreConfigAlso4Worker,
};

类似的,对依赖 electron-log 模块的功能函数的处理:

loggerAlso4Worker.js
const { isMainThread, parentPort } = require("node:worker_threads");

const Logger = isMainThread ? require("~/path/to/logger") : null;

/**
 * 同时兼容在主进程、单开工作进程中调用 (typeApi 约定同 Logger 静态方法名称)
 * @param {"error" | "info"} typeApi
 */
function loggerAlso4Worker(typeApi, ...args) {
  if (isMainThread) {
    return Logger[typeApi](...args);
  }
  parentPort.postMessage({
    loggerDataArgs: ["<WORKER-LOGGER>: ", ...args],
    loggerType: typeApi,
    type: "logger",
  });
}

module.exports = {
  loggerAlso4Worker,
};

以上,是在 electron 桌面应用开发中,某些场景中,可能会遇到这样情况:同一变量值、或导出模块(如类的实例)被主进程、工作进程同时调用,(BUT, 该变量值、该类实例依赖的第三方模块却不被允许在工作进程中被访问),在这种情况下可行解决方案的实践分享

electron 应用开发,在工作进程中访问主进程中变量值的实践分享

https://infen.cc/loc-blog/29_way-to-get-value-in-work-threads[Copy]
本文作者
Helen4i, TC
创建/发布于
Published On
更新/发布于
Updated On
许可协议
CC BY-NC-SA 4.0

转载或引用本文时请遵守“署名-非商业性使用-相同方式共享 4.0 国际”许可协议,注明出处、不得用于商业用途!分发衍生作品时必须采用相同的许可协议。