项目引入 Node 原生插件(.node) 后,出现高崩溃率、主进程阻塞问题。
通过 child_process + fork 子进程 实现插件保活,保证系统稳定运行。
【process】
全局对象,直接用,不用require
代表当前运行的Node.js进程
一个应用启动后只有一个主进程
作用:获取进程信息、环境变量、程序退出、系统信号监听等
【child process】
Node.js内置模块,必须require('child_process')
作用:在主进程中创建独立子进程
子进程可以执行:其他Node脚本、JS/TS、shell命令、Native插件
目的:解决单线程阻塞、利用多核CPU、崩溃隔离,保证主进程稳定
【fork子进程机制】(保活核心)
-
fork作用:
- 创建全新独立的Node.js进程
- 运行指定的JS/TS文件
- 父子进程完全隔离、不共享内存
-
process指向规则(关键)
- fork外部文件:process = 主进程
- fork内部文件:process = 当前子进程
【electron为什么要把Native插件放进fork子进程】
-
- 避免阻塞主进程 / UI线程
- 插件崩溃不会导致整个应用崩溃,可自动重启
- 主进程职责更清晰:只负责生命周期管理+IPC转发
- 实现插件保活,提升应用稳定性
【核心总结】
Nodejs是单线程,主进程若做大量计算或插件逻辑会直接阻塞,卡死甚至崩溃
而child_process可以:
-
- 开启新进程,分担密集型任务
- 子进程崩溃不影响主进程
- 充分利用多核CPU
- 实现崩溃自动重启(保活机制)
【实践】
插件相关API必须放在fork子进程中,主进程只负责
-
- 启动子进程
- 管理子进程生命周期
- 将子进程事件转发给渲染进程
【好处】
-
- 避免native模块阻塞主进程的UI线程
- native模块崩溃不会导致整个Electron应用崩溃(子进程可以重启)
- 主进程代码更清晰,只处理 IPC 转发
fork的作用:启动一个全新的process(新进程),运行指定文件,父子是两个完全独立的进程
结构图:
代码示例:
keepAlive:【主进程fork + 保活】
import { fork } from 'child_process';
const workerPath = this.isDev
?
path.join(__dirname, 'nodeplugin.ts')
:
path.join(__dirname, 'nodeplugin.js');
// 启动子进程
const worker = fork(
workerPath,
[],
{
execArgv: isDev ? ['-r', 'ts-node/register/transpile-only'] : [],
env: {
...process.env,
ELECTRON_IS_PACKAGED: isDev ? 'true' : 'false'
}
}
);
worker.on('spawn', () => {
// 触发通信:向子进程发送通信
worker!.send({type: '', data: null})
})
worker.on('message', (msg) => {
console.log('插件返回:', msg);
});
worker.on('exit', (code) => {
console.log('插件挂了,自动重启');
restartWorker();
});
import { fork } from 'child_process';
const isDev = process.env.NODE_ENV === 'development';
let worker;
function startWorker() {
const workerPath = isDev
?
path.join(__dirname, 'nodeplugin.ts')
:
path.join(__dirname, 'nodeplugin.js');
// 启动子进程
worker = fork(
workerPath,
[],
{
execArgv: isDev
?
['-r', 'ts-node/register/transpile-only']
:
[],
env: {
...process.env,
ELECTRON_IS_PACKAGED: isDev ? 'true' : 'false'
}
}
);
worker.on('spawn', () => {
// 触发通信:向子进程发送通信
worker!.send({type: '', data: null})
})
worker.on('message', (msg) => {
console.log('插件返回:', msg);
});
worker.on('exit', (code, signal) => {
// 非正常退出
if (code !== 0) {
console.warn(`[蓝牙进程] 异常退出 code=${code},准备重启`);
restartWorker();
} else {
console.log(`[蓝牙进程] 正常退出`);
}
});
}
function restartWorker() {
if(!worker) return;
// 清理旧 worker
worker.removeAllListeners();
worker.kill();
worker = null;
// 重新启动
startWorker()
}
pluginNode.ts【子进程】
const native = require('./xxx.node');
// 接收主进程消息
process.on('message', (msg) => {
console.log(msg)
// 处理逻辑
switch(msg.type) {
case 'case1':
...
break;
.
.
.
}
// 触发通信:向外层woker发送数据,实现回传
process.send(result);
});
【父子进程通信坑点:Uint8Array类型丢失】
在跨进程传输后Uint8Array变成了普通对象,导致无法使用byteLength等属性
【现象】:
- 子进程里:Uint8Array(39) [...] (类型化数组)
- 父进程收到:**{ '0': 0, '1':0 ... } **(普通对象)
- 未做任何转换 → 但类型自动改变。
【根本原因】:
- 进程之间不能直接共享内存
- 跨进程 IPC 必须经过序列化,才能跨进程传递
- Node 对 TypedArray / Uint8Array 的序列化规则:自动转为「索引 - 值」普通对象
子进程:Uint8Array
↓
IPC 序列化(自动)
↓
父进程:普通对象 { 0:xx, 1:xx ... }
【解决方案】
传输时用: const value = Array.from(payload)
接收时用: const payloadArray = new Uint8Array(value);
【最终总结】
-
主进程 = process,全局唯一,严禁阻塞
-
child_process.fork() = 创建独立子进程
-
Native插件必须放在子进程,防止崩溃拖垮整个应用
-
子进程崩溃 → 主进程自动重启 → 实现保活
-
Uint8Array跨进程会变对象 → 使用Array.form / new Uint8Array还原