Electron使用worker

您所在的位置:网站首页 webpack使用webwork Electron使用worker

Electron使用worker

2024-07-10 05:50| 来源: 网络整理| 查看: 265

Electron使用worker_thread多线程

JS以其单线程事件循环模型在UI和IO密集型应用中有着很好的表现相性。 但是当我们用Electron构建桌面应用时,不可避免地需要进行某些CPU密集的任务。

为了解决JS多线程的问题,WebWorker应运而生(MDN WebWorker)。 WebWorker是独立的工作者线程,它与主线程不共享任何状态,只通过事件通信。

类似的,在Node世界中,标准库为我们提供了worker_threads模块,其实现了与WebWorker相同的工作者。DOC

本文将介绍worker_threads的使用,以及结合ERB讲解如何在项目中让worker平稳地渡过整个构建周期,与TS,Webpack等构建工具适配。

关于ERB的相关知识,可以参考我的上一篇博客

基本用法

WebWorker的使用非常简单,只需要创建一个Worker对象并指定js脚本,工作者线程就已经启动了。

下面是官方文档给出的一个示例

const { Worker, isMainThread, parentPort, workerData } = require('worker_threads'); if (isMainThread) { module.exports = function parseJSAsync(script) { return new Promise((resolve, reject) => { const worker = new Worker(__filename, { workerData: script }); worker.on('message', resolve); worker.on('error', reject); worker.on('exit', (code) => { if (code !== 0) reject(new Error(`Worker stopped with exit code ${code}`)); }); }); }; } else { const { parse } = require('some-js-parsing-library'); const script = workerData; parentPort.postMessage(parse(script)); } 这个例子把主线程和工作线程写到了同一个文件里。首先判断当前线程是否是主线程如果是主线程,则导出一个函数该函数返回一个Promise其创建了Worker来执行任务,并通过workerData提供了参数通过worker对象的事件监听,来执行对应的resolve和reject如果是工作线程,则开始工作从workerData中取得参数执行任务后将结果postMessage到主线程集成到ERB

上面简单的例子展示了worker_thread的基本用法,但是应用到实际Electron项目中却远远不够。

因为从原始代码到最终打包经历了数个环节,typescript/webpack/electron-builder都会对代码和结构产生影响。

首先,我们在项目中创建测试用的worker脚本src/main/worker/debug.ts。

import { threadId, workerData } from 'worker_threads'; const end = Date.now() + workerData; while (Date.now() { const name = path.parse(e).name; workerEntries[`worker/${name}`] = path.join(webpackPaths.srcMainPath, `worker/${name}.ts`); }); const configuration: webpack.Configuration = { //... entry: { main: path.join(webpackPaths.srcMainPath, 'main.ts'), preload: path.join(webpackPaths.srcMainPath, 'preload.js'), ...workerEntries, }, //... }

配置完成后,再次调用npm run build构建,就会发现在release/app/dist/main文件夹下已经有了worker目录及工作脚本了。

asar

虽然build已经通过,但是在最终package中却依然不能使用。 原因是electron-builder默认开启了asar压缩。 worker没有办法找到我们的js文件。

为此,我们需要配置asar忽略这些文件。

{ "build": { "asar": true, "asarUnpack": [ "**\\*.{node,dll}", + "dist\\main\\worker\\*.js" ], } } path

在上两小节webpack和asar中都隐藏了一些关于路径的问题。

webpack打包后主线程只存在main.js,与开发期路径不一致asar打包后,其他代码在app.asar中,而工作脚本在app.asar.unpacked中

所以,我们需要对上面定义的工具方法稍作改造,让它能够在生产模式中兼容。

import path from 'path'; import { Worker, WorkerOptions } from 'worker_threads'; export function newWorker(name: string, options: WorkerOptions = {}) { if (process.env.NODE_ENV === 'development') { const tsFile = path.resolve(path.join(__dirname, `../worker/${name}.ts`)).replace(/\\/g, '/'); return new Worker(` require('ts-node').register(); require('${tsFile}') `, { ...options, eval: true, }); } else { const jsFile = path.resolve(path.join(__dirname, `./worker/${name}.js`)) .replace('app.asar', 'app.asar.unpacked'); return new Worker(jsFile, options); } }

现在,终于算是大功告成,我们的工具方法可以在开发模式和生产模式下完美兼容。 让worker的使用变得非常简单。

import { newWorker } from './util/worker' const worker = newWorker('debug', { workerData: millis, }); 插曲:原生模块

本想事情到此告一段落,可是天不遂人愿,又或者说是幸运的发现了一个问题。 在项目中,我使用了node-expat来解析XML文件,这是一个C++编写的原生模块。

当我在worker中测试使用该模块时,发生了类似如下错误。

Error: Module did not self-register. at Object.Module._extensions..node (internal/modules/cjs/loader.js:731:18) at Module.load (internal/modules/cjs/loader.js:612:32) at tryModuleLoad (internal/modules/cjs/loader.js:551:12) at Function.Module._load (internal/modules/cjs/loader.js:543:3)

为了搞清楚原因,我就单独写了一个js文件,脱离electron测试。奇怪的是,在单独测试的worker中可以正常加载node-expat并解析XML。

在经过了反复的尝试和资料的查找后,终于定位了原因。 NodeJS原生模块需要启用context aware才能在一条进程中多次加载。 详见该Issue

在我的测试中不报错就是因为我在测试中只在worker中使用了,如果在主进程加入一行require,则能够重现该错误。

在node-expact库中有人在两年前提交了context aware功能的改动(只需要修改一行),但是由于没有测试用例,该PR很遗憾地被关闭了。

对于这种情况,恐怕就只能选用非原生模块或者是兼容的原生模块了。(我后来选用了sax模块替换node-expat)

结语

本文以ERB为例,详细讲解和演示了worker_thread的用法。 即使你不使用ERB,使用其他的框架,其涉及的工具也都大同小异,可以用相同的思路逐一攻破。



【本文地址】


今日新闻


推荐新闻


CopyRight 2018-2019 办公设备维修网 版权所有 豫ICP备15022753号-3