Electron开发重点

Electron 开发重点知识体系梳理

Electron 是基于 ChromiumNode.js 的跨平台桌面应用开发框架,核心逻辑是用 Web 技术(HTML/CSS/JS)开发界面,同时通过 Node.js 调用原生系统能力。以下是从基础架构到工程化落地的完整知识体系梳理。


项目汇总

code/all/e-streamlabs-obs-v0.21.2x 导播助手
code/all/e-live(已下线)
code/all/e-ppt(已下线)
code/all/e-live-streaming 直播助手
code/all/e-ipub 融合出版平台
code/all/e-ysy 亿童幼师云


一、核心架构与基础概念

1. 双进程模型(核心基石)

Electron 应用由两类进程组成,二者职责严格分离,通过 IPC 通信协作:

进程类型 核心职责 运行环境 入口文件
主进程 (Main Process) 应用生命周期管理、窗口创建、原生系统调用、全局状态管理 Node.js 环境 package.jsonmain 字段指定的文件(如 main.js
渲染进程 (Renderer Process) 页面渲染、UI 交互、业务逻辑执行 Chromium 环境(类似浏览器标签页) 每个 BrowserWindow 加载的 HTML/JS 文件

2. 关键模块与对象

  • app 模块:控制应用的生命周期(仅主进程可用)。
  • BrowserWindow 模块:创建和管理应用窗口(仅主进程可用)。
  • webContents 对象:渲染进程的核心对象,负责页面渲染和与渲染进程通信(主进程中通过 win.webContents 访问)。

二、进程间通信 (IPC) —— 开发核心难点

Electron 双进程模型下,主进程与渲染进程、渲染进程之间的通信是最频繁且最容易出错的环节,必须掌握以下模式:

1. 核心通信模块

  • **ipcMain**:主进程模块,用于监听渲染进程发送的消息。 win.webContents.send(‘channel-name’, data); 主进程发送消息给渲染进程
  • **ipcRenderer**:渲染进程模块,用于向主进程发送消息、监听主进程回复。

2. 常用通信模式

(1)渲染进程 → 主进程(单向通知)

场景:渲染进程触发主进程执行某个操作(如打开文件选择框、创建新窗口)。

1
2
3
4
5
6
7
8
9
10
// 渲染进程 (renderer.js)
const { ipcRenderer } = require('electron');
ipcRenderer.send('open-file-dialog');

// 主进程 (main.js)
const { ipcMain, dialog } = require('electron');
ipcMain.on('open-file-dialog', async (event) => {
const result = await dialog.showOpenDialog({ properties: ['openFile'] });
console.log(result.filePaths);
});

(2)渲染进程 → 主进程 → 渲染进程(双向请求/响应)

场景:渲染进程请求主进程执行异步操作并返回结果(如读取本地文件、调用系统 API)。

1
2
3
4
5
6
7
8
9
10
11
12
// 渲染进程 (renderer.js)
const { ipcRenderer } = require('electron');
async function readFile() {
const content = await ipcRenderer.invoke('read-local-file', '/path/to/file.txt');
console.log(content);
}

// 主进程 (main.js)
const { ipcMain, fs } = require('electron');
ipcMain.handle('read-local-file', async (event, filePath) => {
return fs.promises.readFile(filePath, 'utf-8');
});

(3)主进程 → 渲染进程(主动推送)

场景:主进程监听系统事件(如托盘点击、网络变化),主动通知渲染进程更新 UI。

1
2
3
4
5
6
7
// 主进程 (main.js)
win.webContents.send('system-tray-clicked', { timestamp: Date.now() });

// 渲染进程 (renderer.js)
ipcRenderer.on('system-tray-clicked', (event, data) => {
console.log('托盘被点击:', data);
});

(4)渲染进程 <-> Webview进程

场景:渲染进程与 Webview 进程之间需要通信(如 Webview 加载完成后,渲染进程通知 Webview 执行某个操作)。
postMessage 方法:渲染进程通过 webContents.postMessage 发送消息,Webview 进程通过 window.addEventListener('message', (event) => { ... }) 监听。

在 Electron 中,<webview> 标签内的“访客”内容(即加载的第三方网页)运行在一个独立的、隔离的渲染进程中,拥有自己的 window 对象和执行环境。因此,它无法直接访问您主应用渲染进程(宿主页面)的全局对象或 IPC 通道。

核心在于渲染进程(宿主页面)如何与 webview 中的内容通信。主要有两种方式:一种是基于 postMessage 的标准 Web API,另一种是基于 Electron IPC 的专用方法。如果您希望通信双方代码耦合度低、逻辑通用,postMessage 是更合适的选择。

以下是两种通信方式的详细实现和对比:

通信方式 发送方 接收方 核心机制 典型使用场景
postMessage 宿主页面 webview 内页面 宿主页面通过 webview.executeJavaScript() 注入脚本,调用 window.postMessage 通信双方都需要监听标准的 message 事件,代码逻辑与普通网页一致。
webview 内页面 宿主页面 webview 内页面直接调用 window.postMessage,宿主页面监听 webviewipc-message 事件 。 用于 webview 内页面向上通知宿主页面。
IPC (ipcRenderer) 宿主页面 webviewpreload 脚本 宿主页面调用 webview.send() 发送 IPC 消息 。 webview 内页面通过 preload 脚本暴露有限的、安全的 API,进行复杂或需要 Node.js 能力的通信。
preload 脚本 宿主页面 webviewpreload 脚本通过 ipcRenderer.sendToHost() 回复,宿主页面监听 webviewipc-message 事件 。 用于 preload 脚本向宿主页面发送消息。

方案一:基于 postMessage 的双向通信

这种方式最接近 Web 标准。关键在于宿主页面需要借助 executeJavaScriptwebview 的上下文中执行代码,从而建立连接。

1. WebView 内页面发送消息给宿主页面

这是最直接的方式。webview 内部的网页像平时一样使用 postMessage,宿主页面通过监听 webview 标签的 ipc-message 事件来接收。

  • webview 内的页面(访客页)中:

    1
    2
    3
    4
    5
    // 这是 webview 内加载的网页中的代码
    function sendMessageToHost() {
    // 直接向父窗口发送消息
    window.postMessage({ type: 'FROM_WEBVIEW', text: 'Hello from inside!' }, '*');
    }
  • 在宿主页面(渲染进程)中:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    // 这是包含 <webview> 标签的页面代码
    const webview = document.querySelector('webview');

    webview.addEventListener('ipc-message', (event) => {
    // 注意:通过 ipc-message 接收到的 event 对象结构与标准 MessageEvent 不同。
    // 实际通过 postMessage 发送的复杂数据对象,需要通过其他方式(如 executeJavaScript 的回调)才能完整获取。
    // 一个更可靠的方式是结合 executeJavaScript 在 webview 内设置监听器。
    console.log('Received from webview:', event.channel, event.args);
    });

    // 更推荐的方式:通过 executeJavaScript 在 webview 内部设立监听器
    webview.addEventListener('dom-ready', () => {
    webview.executeJavaScript(`
    window.addEventListener('message', (event) => {
    // 可以将接收到的消息通过其他方式转发给宿主,例如:
    console.log('Webview内部收到消息:', event.data);
    // 但要将此消息发回给宿主,仍需依赖下面的“宿主页面发送消息给WebView内页面”的方法。
    });
    `);
    });

    说明:直接通过 ipc-message 接收 postMessage 的数据可能会受限。一个更稳健的模式是让宿主页面通过 executeJavaScriptwebview 内注入一个全局函数,该函数再通过 postMessage 将数据传回,而宿主则通过监听同一个 ipc-message 来捕获。不过,下面的“宿主发送给webview”方案通常更常用。

2. 宿主页面发送消息给 WebView 内页面

宿主页面通过 executeJavaScriptwebview 的上下文中执行代码,从而调用其内部的 postMessage 方法 。

  • 在宿主页面(渲染进程)中:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    const webview = document.querySelector('webview');

    function sendMessageToWebview() {
    if (webview) {
    const message = { type: 'FROM_HOST', text: 'Hello from host page!' };
    // 关键步骤:构造一段脚本,在 webview 内部执行 window.postMessage
    const script = `window.postMessage(${JSON.stringify(message)}, '*');`;
    webview.executeJavaScript(script);
    }
    }

    // 确保 webview 已加载完成
    webview.addEventListener('dom-ready', () => {
    // 在 webview 内部设置一个监听器,以接收来自外部的消息
    webview.executeJavaScript(`
    window.addEventListener('message', (event) => {
    console.log('Webview received:', event.data);
    // 在这里处理接收到的消息
    if (event.data.type === 'FROM_HOST') {
    // ... 执行相关操作
    }
    });
    `);
    });
  • webview 内的页面(访客页)中:

    1
    2
    3
    4
    5
    // 这是 webview 内加载的网页中的代码,它只需要监听标准的 message 事件即可
    window.addEventListener('message', (event) => {
    console.log('Message received from host:', event.data);
    // 处理来自宿主页面的消息
    });
方案二:基于 ipcRendererpreload 的双向通信

这种方式利用了 Electron 的进程间通信能力,功能更强大,但需要在 webview 上启用 nodeintegration 或通过 preload 脚本安全地暴露 API。

1. 宿主页面发送消息给 WebView(通过 webview.send

宿主页面调用 webview.send() 发送一个 IPC 消息,该消息可以在 webviewpreload 脚本中通过 ipcRenderer 接收 。

  • 在宿主页面中:

    1
    2
    3
    4
    5
    const webview = document.querySelector('webview');
    webview.addEventListener('dom-ready', () => {
    // 向 webview 发送 IPC 消息,频道名为 'ping'
    webview.send('ping', 'Hello from host!');
    });
  • preload.js(为 webview 指定的预加载脚本)中:

    1
    2
    3
    4
    5
    6
    7
    8
    const { ipcRenderer } = require('electron');

    ipcRenderer.on('ping', (event, message) => {
    console.log('Received ping in preload:', message); // 输出: Received ping in preload: Hello from host!

    // 可以向宿主页面回复消息
    ipcRenderer.sendToHost('pong', 'Message received in webview!');
    });
2. WebView 内页面发送消息给宿主页面(通过 sendToHost

webview 内的 preload 脚本可以通过 ipcRenderer.sendToHost() 向宿主页面发送消息,宿主页面监听 webviewipc-message 事件 。

  • preload.js 中:

    1
    2
    3
    4
    5
    6
    const { ipcRenderer } = require('electron');

    // 假设在某个时机,需要通知宿主页面
    function notifyHost() {
    ipcRenderer.sendToHost('custom-event', { data: 'Something happened in webview' });
    }
  • 在宿主页面中:

    1
    2
    3
    4
    5
    6
    const webview = document.querySelector('webview');
    webview.addEventListener('ipc-message', (event) => {
    if (event.channel === 'custom-event') {
    console.log('Received from webview preload:', event.args[0]); // 输出: Received from webview preload: { data: 'Something happened in webview' }
    }
    });

    Webview页面调用宿主页面转异步,800ms则为超时

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    // 获取当前页面tab信息 -- 800毫秒未响应,则定义为超时
    export const getTabInfo = () => {
    return new Promise((resolve, reject) => {
    if (!isInTab) {
    reject('当前页面不在tab页内')
    }
    YTIPUB.sendToHost(
    JSON.stringify({
    event: 'getTab',
    }),
    )

    const listener = (event) => {
    try {
    if (typeof event.data !== 'string') return
    const data = JSON.parse(event.data)
    if (data.event === 'getTab' && typeof data.tab === 'string') {
    const tab = JSON.parse(data.tab)
    console.log('%c获取页面tab信息成功:', 'background: #f00', tab)
    window.removeEventListener('message', listener)
    resolve(tab)
    }
    } catch (err) {
    window.removeEventListener('message', listener)
    reject(err)
    }
    }
    window.addEventListener('message', listener)

    setTimeout(() => {
    window.removeEventListener('message', listener)
    resolve('获取tab超时')
    }, 800)
    })
    }

(5)主进程 -> Webview进程

同(3)主进程 → 渲染进程(主动推送)

(6)Webview进程 <-> Webview进程

多标签实现时,每个标签页都是一个独立的 Webview 进程,创建时都有一个uuid,它们之间通过渲染进程中转通信。渲染进程作为中间层使用event-mitter库,负责接收来自 Webview 进程的消息广播出去或回复指定目标uuid的消息,并将其转发给其他 Webview 进程。

3. 上下文隔离下的安全通信(现代 Electron 标准做法)

为了安全,**现代 Electron 必须启用 contextIsolation 和禁用 nodeIntegration**,此时渲染进程无法直接访问 ipcRenderer,需通过 Preload 脚本 暴露安全 API:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
// 主进程创建窗口时配置
const win = new BrowserWindow({
webPreferences: {
nodeIntegration: false, // 禁用渲染进程 Node.js 集成
contextIsolation: true, // 启用上下文隔离(默认开启)
preload: path.join(__dirname, 'preload.js') // 预加载脚本
}
});

// preload.js(预加载脚本,在渲染进程上下文隔离前执行)
// IPC通信配置
export const IPC = {
// 渲染进程向主进程发送异步消息,无结果返回
SEND: {
__needIpcMainOn: true,
PLUGIN: 'pluginSend',
MAXIMIZE: 'maximize', // 最大化
UNMAXIMIZE: 'unmaximize', // 还原
MINIMIZE: 'minimize', // 最小化
FULLSCREEN: 'fullscreen', // 全屏
CLOSE: 'close', // 关闭
DESTROY: 'destroy', // 退出
RELAUNCH: 'relaunch', // 重启
SET_CONTENT_SIZE: 'setContentSize', // 设置窗口大小
TOGGLE_SCREEN: 'toggleScreen', // 切换窗口:登录、主界面
PERFORMANCE_EXPORT: 'pExport', // 导出性能指标
LOGGER: 'logger', // 写入本地日志
LOG_REPORT: 'logReport', // 写入在线日志
PACKAGING: 'packaging', // 开始打包
DOWNLOAD_URL: 'downloadURL', // 下载
OPEN_EXTERNAL: 'openExternal', // 打开外链
SET_SKIP_TASKBAR: 'setSkipTaskbar', // 是否在任务栏中显示窗口。 默认值为 false.
ELECTRON_UPDATE_INSTALL: 'electronUpdateInstall', // 确认壳进程安装更新
INIT_UPDATER: 'initUpdater', // 初始化更新
REMOVE_FOLDER: 'removeFolder', // 删除文件夹
SIMULATE_CLICK_YTHD: 'simulateClickYTHD', // 模拟点击YTHD文件
FOCUS: 'focus', // mainWindow聚焦
},
// 渲染进程向主进程发送消息,并异步等待结果。可覆盖 once 使用场景
INVOKE: {
__needIpcMainHandle: true,
PLUGIN: 'pluginInvoke', // 插件公共方法
CHECK_NET_ONCE: 'checkNetOnce', // 读取网络状态
RECHECK_NET: 'recheckNet', // 刷新网络
CHECK_NET: 'checkNet', // 检查网络
SAVE_DIALOG_SYNC: 'showSaveDialogSync', // 保存文件选择
OPEN_DIALOG_SYNC: 'showOpenDialogSync', // 调用系统文件选择
COMPUTE_HASH: 'computeHash', // 计算hash
OPEN_FOLDER: 'openFolder', // 打开文件夹
CMS_CONFIG: 'cmsConfig', // 获取cms中配置项
SAVE_BASE64_FILE: 'saveBase64File', // 保存base64文件到本地
GET_DEVICE_ID: 'getDeviceId', // 获取设备唯一ID
CLEAR_OLD_PLUGIN: 'clearOldPlugin', // 清理废弃插件空间
},
// 渲染进程中,webview页面向父页面发送消息
SEND_TO_HOST: {
WEBVIEW_TO_RENDER: 'webviewToRender',
},
// 渲染进程监听来自主进程消息
LISTENER: {
__needIpcRender: true,
PLUGIN: 'pluginListener', // 插件公共监听
MAIN_TO_RENDER_LISTENER: 'mainToRenderListener', // 渲染进程 监听主进程消息
MAIN_TO_WEBVIEW_LISTENER: 'mainToWebviewListener', // webview 监听主进程消息
FFMPEG_STATUS_LISTENER: 'ffmpegStatusListener', // ffmpeg 状态监听
MAIN_IS_MAXIMIZED: 'mainIsMaximized', // 窗口是否最大化
MAIN_IS_FOCUS: 'mainIsFocus', // 窗口是否聚焦
ELECTRON_UPDATE: 'electronUpdate', // 壳进程更新
PLUGINS_UPDATE: 'pluginsUpdate', // 插件更新
INIT_ABNORMAL: 'initAbnormal', // 初始化异常处理
},
};
// shell、app等 api 在 electron-preload.js 不可用,可以执行多次exposeInMainWorld
import { contextBridge, ipcRenderer } from 'electron';
import { IPC, props, IS_PRODUCTION, appPackage } from './config';
import { PATH } from './config-path';
import { IPCValue } from './types/index';
import { pathToFileURL } from './utils/index';

// eslint-disable-next-line @typescript-eslint/no-explicit-any
const exposeParams: any = {
platform: appPackage.platformName,
send: ipcRenderer.send,
invoke: ipcRenderer.invoke,
sendToHost: ipcRenderer.sendToHost,
isProduction: IS_PRODUCTION,
IPC,
PATH,
props,
preload: pathToFileURL(PATH.APP_PRELOAD),
};

Object.values(IPC).forEach((item: IPCValue) => {
const { __needIpcRender, ...rest } = item;
__needIpcRender &&
Object.values(rest).forEach((name: string | boolean) => {
if (typeof name !== 'string') return;
exposeParams[name] = (callback: FunctionConstructor) => {
ipcRenderer.on(name, (_event, arg) => {
callback?.(arg);
});
};
});
});

console.log('JS-SDK exposeParams:', exposeParams);
contextBridge.exposeInMainWorld('YTIPUB', exposeParams);

主进程调用

1
2
3
4
win.webContents.send(IPC?.LISTENER?.ELECTRON_UPDATE, {
type,
payload
})

渲染进程或Webview监听

1
YTIPUB && YTIPUB[IPC.LISTENER.INIT_ABNORMAL]?.(handleAbnormal);

三、窗口与应用生命周期管理

1. BrowserWindow 窗口配置

创建窗口时的核心配置项(webPreferences 是安全重点):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
const win = new BrowserWindow({
width: 1200,
height: 800,
minWidth: 800,
minHeight: 600,
frame: false, // 无边框窗口(自定义标题栏时用)
transparent: true, // 透明窗口(配合无边框使用)
resizable: true,
webPreferences: {
// 安全配置(必看)
nodeIntegration: false,
contextIsolation: true,
preload: path.join(__dirname, 'preload.js'),
// 开发环境配置
devTools: true // 生产环境建议禁用
}
});

// 加载页面
win.loadURL('https://example.com'); // 加载远程 URL
win.loadFile('index.html'); // 加载本地 HTML 文件

2. 窗口类型与层级

  • 父子窗口:通过 parent 选项创建,子窗口始终显示在父窗口上方。
    1
    const childWin = new BrowserWindow({ parent: win, modal: true });
  • 模态窗口:设置 modal: true,阻塞父窗口交互(常用于弹窗、对话框)。

对于showSaveDialogSync、showOpenDialogSync同样可以设置parent,确保对话框显示在主窗口上方。

3. 应用生命周期(app 模块事件)

掌握核心生命周期事件,控制应用启动、退出逻辑:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
const { app } = require('electron');

// 1. 应用准备就绪(创建窗口的最佳时机)
app.whenReady().then(() => {
createWindow();
// macOS 特性:点击 Dock 图标重新创建窗口
app.on('activate', () => {
if (BrowserWindow.getAllWindows().length === 0) createWindow();
});
});

// 2. 所有窗口关闭时(Windows/Linux 直接退出应用)
app.on('window-all-closed', () => {
if (process.platform !== 'darwin') app.quit();
});

// 3. 应用退出前(清理资源、保存数据)
app.on('before-quit', (event) => {
// 可在此阻止退出:event.preventDefault();
});

3. 单例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
const { app, BrowserWindow } = require('electron');
let mainWindow;

// 单例锁判断
if (!app.requestSingleInstanceLock()) {
app.quit();
// 确保进程完全退出
process.exit();
} else {
// 仅开发环境忽略证书错误,生产环境需移除
if (process.env.NODE_ENV === 'development') {
app.commandLine.appendSwitch('ignore-certificate-errors');
}
app.commandLine.appendSwitch('autoplay-policy', 'no-user-gesture-required');

// 监听第二个实例启动事件,激活已有窗口
app.on('second-instance', () => {
if (mainWindow) {
if (mainWindow.isMinimized()) mainWindow.restore();
mainWindow.focus();
}
});

// 正常创建窗口逻辑
app.whenReady().then(() => {
mainWindow = new BrowserWindow({ width: 800, height: 600 });
mainWindow.loadFile('index.html');
});
}

四、原生能力与系统交互

Electron 的核心优势是通过 Node.js 和内置模块调用原生系统能力,以下是高频使用场景:

1. 文件系统操作

主进程可直接使用 Node.js 的 fspath 模块,结合 dialog 模块实现文件选择:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
const { dialog } = require('electron');
const fs = require('fs/promises');

// 打开文件选择框
async function selectAndReadFile() {
const { canceled, filePaths } = await dialog.showOpenDialog({
properties: ['openFile'],
filters: [{ name: 'Text Files', extensions: ['txt'] }]
});
if (!canceled) {
const content = await fs.readFile(filePaths[0], 'utf-8');
return content;
}
}

2. 菜单与托盘

  • 应用菜单:通过 Menu 模块创建自定义菜单栏(Windows/Linux 在窗口顶部,macOS 在屏幕顶部)。

    1
    2
    3
    4
    5
    6
    7
    const { Menu } = require('electron');
    const template = [
    { label: '文件', submenu: [{ label: '打开', click: () => console.log('打开文件') }] },
    { label: '编辑', submenu: [{ role: 'copy' }, { role: 'paste' }] } // 使用内置 role
    ];
    const menu = Menu.buildFromTemplate(template);
    Menu.setApplicationMenu(menu);

    当然我们也可以在windows完全组件自定义,不使用Menu,Dom节点上配置样式即可

    1
    -webkit-app-region: drag; // no-drag
  • 系统托盘:通过 Tray 模块添加系统托盘图标和右键菜单。

    1
    2
    3
    4
    5
    6
    7
    8
    const { Tray, Menu } = require('electron');
    const tray = new Tray('path/to/icon.png');
    const contextMenu = Menu.buildFromTemplate([
    { label: '显示窗口', click: () => win.show() },
    { label: '退出', click: () => app.quit() }
    ]);
    tray.setToolTip('My Electron App');
    tray.setContextMenu(contextMenu);

3. 系统集成

  • 通知:使用 HTML5 Notification API(渲染进程)或 Node.js 模块(主进程)。
  • 剪贴板:通过 clipboard 模块读写系统剪贴板。
  • 外部链接:通过 shell 模块在默认浏览器中打开链接(避免在 Electron 窗口中打开外部页面)。
    1
    2
    const { shell } = require('electron');
    shell.openExternal('https://github.com');

3. 注册系统协议

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import { app } from 'electron'
import path from 'path'
import { IS_PRODUCTION, SCHEME_PROTOCOL } from '../config'
import { logger } from '../basic/logger'

/**
* @description 注册协议
* Windows、macOS
* https://www.electronjs.org/zh/docs/latest/api/app#appsetasdefaultprotocolclientprotocol-path-args
*/
export function setDefaultProtocol() {
logger.debug('setDefaultProtocol', {
'process.defaultApp': process.defaultApp,
'process.argv': process.argv,
'scheme.protocol': SCHEME_PROTOCOL,
})
if(!IS_PRODUCTION) return
process.defaultApp ?
process.argv.length >= 2 && app.setAsDefaultProtocolClient(SCHEME_PROTOCOL, process.execPath, [path.resolve(process.argv[1])]) :
app.setAsDefaultProtocolClient(SCHEME_PROTOCOL)
}

4. 注册文件扩展

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
/**
* @description 获取文件关联配置
*/
function getFileAssociations() {
let suffix = ''
if (process.platform === 'linux') {
suffix = 'png'
} else if (process.platform === 'darwin') {
suffix = 'icns'
} else {
suffix = 'ico'
}
return [
{
ext: 'ythd',
// windows-only
description: 'application/ythd',
// linux-only
mimeType: 'application/x-ythd',
name: 'ythd File',
// macOS-only(linux也会生效) Editor, Viewer, Shell, or None
role: 'Viewer',
icon: `./public/ythd.${suffix}`,
},
];
}

五、工程化:打包与分发

开发完成后,需将 Electron 应用打包为各平台的安装包(.exe.dmg.AppImage 等),**electron-builder** 是目前最主流的打包工具。

1. 快速配置

package.json 中添加 build 配置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
// Full list of options: https://v2.quasar.dev/quasar-cli-vite/developing-electron-apps/configuring-electron
electron: {
// extendElectronMainConf (esbuildConf)
// extendElectronPreloadConf (esbuildConf)

inspectPort: 5858,

bundler: 'builder', // 'packager' or 'builder'

packager: {
// https://github.com/electron-userland/electron-packager/blob/master/docs/api.md#options
// OS X / Mac App Store
// appBundleId: '',
// appCategoryType: '',
// osxSign: '',
// protocol: 'myapp://path',
// Windows only
// win32metadata: { ... }
},

builder: {
// https://www.electron.build/configuration/configuration
appId,
compression: 'normal',
// https://www.electron.build/configuration/configuration
productName: process.platform === 'darwin' ? description : productName,
directories: {
output: 'build',
},
nsis: {
// 配置文档
// https://www.electron.build/configuration/nsis
guid: appId,
warningsAsErrors: false, // nsis警告变错误(防止警告变成报错无法打包)
deleteAppDataOnUninstall: false,
// include: './install.nsh',
oneClick: false, // 是否一键安装
allowToChangeInstallationDirectory: true, // 允许修改安装目录
perMachine: true, // 是否为每个用户安装
allowElevation: true, // 允许权限提升
createDesktopShortcut: true, // 创建桌面快捷方式
runAfterFinish: true, // 安装结束后启动应用
createStartMenuShortcut: true,
// unicode: true,
// installerIcon: './public/installer.ico',
// uninstallerIcon: './public/installer.ico',
// installerHeader: './public/installer.ico', // 默认.bmp格式文件
// installerHeaderIcon: './public/installer.ico',
// installerSidebar: './public/sidebar.bmp',
// uninstallerSidebar: './public/sidebar.bmp',
shortcutName: description,
},
publish,
files: [],
dmg: {
contents: [
{
x: 410,
y: 150,
type: 'link',
path: '/Applications',
},
{
x: 130,
y: 150,
type: 'file',
},
],
},
mac: {
icon: `./src-electron/icons${isDsy ? '-dsy' : ''}/icon.icns`,
artifactName:
productName + '_${version}_' + props.buildEnv + '.${ext}',
// identity: 'P5V8WJFBD3',
// category: 'Education',
target: [
// {
// target: 'zip',
// arch: [
// // 'arm64',
// 'x64',
// ],
// },
{
target: 'dmg',
arch: [
// 'arm64',
'x64',
],
},
],
},
win: {
icon: `./src-electron/icons${isDsy ? '-dsy' : ''}/icon.ico`,
artifactName:
productName + '_${version}_' + props.buildEnv + '.${ext}',
// requestedExecutionLevel: 'highestAvailable',
target: {
target: 'nsis',
arch: ['ia32'],
},
...getSignConfig(),
},
linux: {
icon: `src-electron/icons${isDsy ? '-dsy' : ''}`,
artifactName:
productName + '_${version}_' + props.buildEnv + '.${ext}',
target: ['deb'],
desktop: {
Icon: `/usr/share/icons/hicolor/512x512/apps/${
isDsy ? 'ertongdongshangyun' : 'yitongyoushiyun'
}.png`,
Name: description,
},
// 应用分类
category: 'Education',
},
extends: null,
extraResources: [
{
from: `./${unpackedName}`,
to: `./${unpackedName}`,
},
],
fileAssociations: getFileAssociations(),
beforePack() {
if (isDsy) {
// 1. 替换 favicon.ico
fs.copyFileSync(
path.resolve(__dirname, './src-electron/icons-dsy/icon.ico'),
path.resolve(__dirname, './dist/electron/UnPackaged/favicon.ico')
);
// 2. 复制 icons 文件夹
fs.cpSync(
path.resolve(__dirname, './src-electron/icons-dsy'),
path.resolve(__dirname, './dist/electron/UnPackaged/icons'),
{
recursive: true,
}
);
}
},
},

extendPackageJson(pkg) {
const { forOwn } = require('lodash');
forOwn(packageJson[project], (value, key) => {
pkg[key] = value;
});

pkg.ysy = undefined;
pkg.dsy = undefined;
delete pkg.ysy;
delete pkg.dsy;
},
},

windows签名

配置electron.builder配置,获取打包签名配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
/**
* @description 获取打包签名配置
*/
function getSignConfig() {
const {
CODE_SIGN_CERT: certificateFile,
CODE_SIGN_CERT_PASSWORD_PSW: certificatePassword,
} = process.env;
const canSign =
certificateFile && certificatePassword && !(version.split('.')[2] % 2); // 偶数版本签名
console.log(
'certificateFile',
certificateFile,
'certificatePassword',
certificatePassword,
`${version}`,
!(version.split('.')[2] % 2)
);
const signConfig = canSign
? {
verifyUpdateCodeSignature: false,
signingHashAlgorithms: ['sha256'],
signDlls: false,
// http://sha256timestamp.ws.symantec.com/sha256/timestamp
// http://timestamp.digicert.com
// http://timestamp.globalsign.com/scripts/timstamp.dll
rfc3161TimeStampServer: 'http://timestamp.digicert.com',
certificateFile,
certificatePassword,
}
: {};
console.log('signConfig', signConfig);
return signConfig;
}

macOS签名 & linux签名-统信UOS、麒麟

以下是 electron-builder 针对 macOSLinux 的签名配置详细指南,分为两部分说明:

一、macOS 签名与公证配置
macOS 签名分为两步:代码签名(Code Signing)公证(Notarization)(macOS 10.14.5+ 必须公证才能在未授权设备上运行)。

1. 前置准备
  • Apple Developer 账号:需加入 Apple Developer Program(年费 99 美元)。
  • 证书创建
    1. 登录 Apple Developer 证书页面
    2. 创建 Developer ID Application 类型的证书(用于发布到 App Store 外的应用)。
    3. 下载证书并双击安装到 Mac 的 Keychain(钥匙串访问)中。
  • 公证所需信息
    • Apple ID(邮箱)。
    • App-specific 密码:在 Apple ID 管理页 生成(用于替代主密码,避免泄露)。
    • Team ID:在 Apple Developer 会员详情页查看。
2. 基础签名配置

electron-builder 配置中添加 mac 签名相关字段:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// electron-builder 配置(如 vue.config.js、electron-builder.json 等)
module.exports = {
mac: {
// ... 其他配置(icon、target 等)

// 1. 签名身份(从 Keychain 中选择证书)
identity: 'Developer ID Application: Your Team Name (TeamID)',
// 或通过环境变量 CSC_NAME 指定(推荐,避免硬编码)

// 2. Hardened Runtime(公证必需,开启后需配置 entitlements)
hardenedRuntime: true,
entitlements: 'build/entitlements.mac.plist', // 权限配置文件
entitlementsInherit: 'build/entitlements.mac.plist',

// 3. 公证配置(macOS 10.14.5+ 必需)
notarize: {
teamId: 'Your TeamID', // Apple Team ID
},
},
// ... 其他平台配置
};

补充说明

  • 环境变量替代方案(推荐用于 CI/CD):

    • CSC_NAME:证书名称(如 Developer ID Application: ...)。
    • CSC_LINK:证书文件路径(.p12 格式),配合 CSC_KEY_PASSWORD 使用。
    • APPLE_ID:Apple ID 邮箱。
    • APPLE_ID_PASSWORD:App-specific 密码。
    • APPLE_TEAM_ID:Team ID。
  • entitlements.mac.plist 示例(需根据应用权限调整):

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    <?xml version="1.0" encoding="UTF-8"?>
    <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
    <plist version="1.0">
    <dict>
    <key>com.apple.security.cs.allow-jit</key>
    <true/>
    <key>com.apple.security.cs.allow-unsigned-executable-memory</key>
    <true/>
    <key>com.apple.security.cs.disable-library-validation</key>
    <true/>
    </dict>
    </plist>

二、Linux 签名配置(以 deb 包为例)

Linux 签名主要针对 .deb 包(Debian/Ubuntu 系),使用 GPG 密钥 签名,确保包的完整性。

1. 前置准备
  • 生成 GPG 密钥对(若已有可跳过):

    1
    gpg --gen-key

    按提示输入姓名、邮箱等信息,生成密钥对(公钥和私钥)。

  • 导出公钥(用于用户验证包):

    1
    gpg --armor --export your-email@example.com > public-key.asc

    public-key.asc 分发给用户,用户可通过 sudo apt-key add public-key.asc 导入信任。

2. electron-builder 配置

linux 配置中添加签名相关字段:

1
2
3
4
5
6
7
8
9
10
11
12
module.exports = {
linux: {
// ... 其他配置(icon、target、category 等)

// 1. 签名配置
sign: {
// 签名算法(可选,默认 sha256)
signingHashAlgorithms: ['sha256'],
},
},
// ... 其他平台配置
};

补充说明

  • 环境变量替代方案(推荐用于 CI/CD):

    • GPG_SIGNER_ID:GPG 密钥的 ID(可通过 gpg --list-secret-keys 查看)。
    • GPG_PASSPHRASE:GPG 密钥的密码(若设置了)。
    • GPG_EXECUTABLE:GPG 可执行文件路径(默认 gpg)。
  • 签名验证:用户安装 .deb 包时,若已导入公钥,会自动验证签名;未导入时会提示警告,需用户手动确认。

三、注意事项

  1. macOS 公证超时:若公证过程超时,可尝试增加 notarize.timeout 配置(单位:秒)。
  2. Linux 多架构签名:若同时构建 x64arm64 包,确保 GPG 密钥对两种架构均有效。
  3. CI/CD 集成
    • macOS:将证书导出为 .p12 文件,通过 CSC_LINKCSC_KEY_PASSWORD 传入 CI。
    • Linux:将 GPG 私钥导出为文件,通过 GPG_PRIVATE_KEY 环境变量传入 CI(需 Base64 编码)。

2. 自动更新

使用 electron-updater 实现应用自动更新(需配合静态文件服务器或 GitHub Releases):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 主进程中配置
const { autoUpdater } = require('electron-updater');

// 检查更新
app.whenReady().then(() => {
autoUpdater.checkForUpdatesAndNotify();
});

// 监听更新事件
autoUpdater.on('update-available', () => {
dialog.showMessageBox({ message: '发现新版本,正在下载...' });
});
autoUpdater.on('update-downloaded', () => {
dialog.showMessageBox({ message: '更新下载完成,点击确定重启应用' }, () => {
autoUpdater.quitAndInstall();
});
});

3. 崩溃上报及分析

  • 崩溃上报:使用 electron-crash-reporterelectron-builder 自动上报崩溃信息。
  • 日志记录:在主进程中使用 electron-log 库记录运行时日志,方便调试。

4. 日志上报

  • 日志记录:在主进程中使用 electron-log 库记录运行时日志,方便调试。
  • 日志上报:将日志上传到服务器,分析崩溃原因(如 阿里云Arms、Sentry、Loggly 等)。

六、性能优化与安全

1. 性能优化重点

  • 渲染进程性能
    • 避免在主线程执行 heavy 计算,使用 Web Workers
    • 优化 DOM 操作,使用虚拟滚动处理长列表。
    • 减少不必要的重绘重排,使用 CSS transformopacity 做动画,硬件加速。
  • 主进程性能
    • 避免在主进程执行阻塞操作(如同步文件读写),尽量使用异步 API。
    • 合理使用 BrowserWindowshow: false 预加载窗口,提升打开速度。
  • 内存管理
    • 及时关闭不再使用的窗口,避免内存泄漏。
    • 移除不再需要的 IPC 监听器(ipcRenderer.removeListener)。

2. 安全红线(必须遵守)

Electron 应用的安全漏洞可能导致远程代码执行,以下是强制安全配置

  1. **始终禁用 nodeIntegration**,启用 contextIsolation
  2. 使用 Preload 脚本 暴露 API,禁止直接在渲染进程使用 Node.js。
  3. 禁止加载不受信任的远程内容,若必须加载,启用 webSecurity(默认开启)。
  4. **设置 Content Security Policy (CSP)**,在 HTML 中添加:
    1
    <meta http-equiv="Content-Security-Policy" content="default-src 'self'; script-src 'self'">
  5. 避免使用 remote 模块(已弃用),改用 IPC 通信。

七、调试与常见问题

1. 调试技巧

  • 渲染进程调试:打开 DevTools(win.webContents.openDevTools()),和 Chrome 调试一致。
  • 主进程调试:使用 --inspect 启动应用,通过 Chrome DevTools 连接:
    1
    electron --inspect=5858 main.js
    然后在 Chrome 中访问 chrome://inspect 进行调试。

2. 常见坑点

  • 跨域问题:渲染进程加载远程接口时可能遇到 CORS,可在主进程通过 webRequest 模块修改响应头,或配置代理。
  • 路径问题:打包后文件路径变化,使用 path.join(__dirname, 'file.txt') 而非相对路径。
  • macOS 签名与公证:macOS 应用必须签名和公证才能在非开发者机器上运行,需在 electron-builder 中配置证书。

3. 性能检测

有的,既有成熟的开源项目可以直接使用,也可以结合你提供的代码进行扩展实现。以下是详细方案:

一、推荐开源项目

1. electron-process-manager
  • 功能:类似 Chrome 任务管理器的 UI 工具,实时展示所有进程(主进程、渲染进程、GPU 进程等)的 CPU、内存占用,支持杀死进程、打开 DevTools。
  • 适用场景:开发阶段快速定位高资源占用进程。
  • 引用:[1]
2. Devtron
  • 功能:Electron 官方调试工具扩展,提供 IPC 消息监控、依赖关系图、事件检查器等。
  • 适用场景:调试 IPC 通信、事件监听器泄漏等问题。
  • 引用:[6]
3. Debugtron
  • 功能:专门用于生产环境的实时调试工具,支持性能分析、错误捕获、日志管理。
  • 适用场景:生产环境问题排查。
  • 引用:[2]

自定义性能监控的代码示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
import { PerformanceObserver, performance, PerformanceEntry } from 'perf_hooks';
import path from 'path';
import fs from 'fs';
import { app, process } from 'electron';
import { logger } from './logger';
import { getLogDirPath } from '../config-path';

// 限制最大条目数,防止内存泄漏
const MAX_ENTRIES = 1000;
export let performanceEntries: PerformanceEntry[] = [];

/**
* @description 性能指标标记(开始/结束)
*/
export function pMark(text: string) {
performance.mark(text);
}

/**
* @description 性能指标计算(需先调用 pMark 创建 startMark 和 endMark)
* @param name 测量名称
* @param startMark 起始标记名
* @param endMark 结束标记名
*/
export function pMeasure(name: string, startMark: string, endMark: string) {
performance.measure(name, startMark, endMark);
}

/**
* @description 性能指标导出(支持增量导出)
*/
export function pExport() {
const data = [...performanceEntries];
// 清空已导出的数据
performanceEntries = [];

fs.writeFile(
path.join(getLogDirPath(), `/performance-${Date.now()}.json`),
JSON.stringify(data, null, 2),
'utf8',
(err) => {
if (err) {
logger.warn('function pExport error:', err);
}
}
);
}

/**
* @description 获取 Electron 进程资源占用
*/
export function getProcessMetrics() {
return {
timestamp: Date.now(),
pid: process.pid,
// 内存占用(MB)
memory: process.getProcessMemoryInfo().then(info => info.workingSetSize / 1024 / 1024),
// CPU 使用率(需通过 app.getAppMetrics() 获取所有进程)
cpu: app.getAppMetrics().find(m => m.pid === process.pid)?.cpu.percentCPUUsage
};
}

/**
* @description 初始化性能指标统计
*/
export function initPerformance() {
performanceEntries = [];

// 1. 初始化标记
pMark('core.init.start');

// 2. 监听 mark 和 measure 事件
const obs = new PerformanceObserver((items) => {
const newEntries = items.getEntries();
performanceEntries.push(...newEntries);

// 超过限制时截取最新数据
if (performanceEntries.length > MAX_ENTRIES) {
performanceEntries = performanceEntries.slice(-MAX_ENTRIES);
}
});
obs.observe({ entryTypes: ['mark', 'measure'] });

// 3. 完成初始化测量
pMark('core.init.end');
pMeasure('core.init', 'core.init.start', 'core.init.end');
}

总结

  • 快速上手:推荐先用 Devtron + electron-process-manager 覆盖基础监控需求。
  • 自定义扩展:在你现有代码基础上,按上述建议修复参数、增加资源限制、补充 Electron 进程指标,可实现更完善的自定义监控。、

4. 崩溃上报及分析

代码示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
import { crashReporter, app } from 'electron';
import { props, appPackage } from '../config';
import { logger, getOsInfo } from './logger';
import Api from '../api/ApiEnums';

/**
* @description 初始化崩溃上报(主进程调用)
*/
export function initCrashReport() {
try {
// 1. 确保在 app ready 之前调用
if (app.isReady()) {
logger.warn('initCrashReport should be called before app ready!');
}

const { env, appVersion } = props;
const osInfo = getOsInfo(); // 提前获取,避免重复调用

// 2. 校验 extra 参数长度(可选但推荐)
const extra = { env, appVersion, ...osInfo };
for (const [key, value] of Object.entries(extra)) {
if (key.length > 39) {
logger.warn(`Crash report extra key too long: ${key}`);
delete extra[key];
}
if (String(value).length > 127) {
extra[key] = String(value).slice(0, 127);
}
}

const params = {
submitURL: `${props.apiBase}${Api.CrashReport}`,
productName: appPackage.productName,
compress: true,
extra,
// 可选:全局参数(所有进程生效,启动后不可修改)
globalExtra: {
appVersion // 若需所有进程统一携带,可放 globalExtra
}
};

logger.debug('initCrashReport params:', params);
crashReporter.start(params);
} catch (err) {
logger.error('initCrashReport error:', err);
}
}

/**
* @description 在渲染进程/子进程中添加额外参数(需通过 contextBridge 暴露)
*/
export function addRendererCrashExtra(key: string, value: string) {
try {
crashReporter.addExtraParameter(key, value);
} catch (err) {
logger.error('addRendererCrashExtra error:', err);
}
}

Dump 文件分析方法

崩溃生成的 .dmp 文件(minidump)需要符号化才能还原出调用栈,以下是常用分析方案:

方案 1:使用 Electron 官方符号 + minidump_stackwalk(本地分析)

步骤:

  1. 下载 Electron 符号文件
    Electron 发布页 下载对应版本的 symbols.zip(如 electron-v28.0.0-darwin-x64-symbols.zip),解压得到 .pdb(Windows)或 .sym(macOS/Linux)文件。

  2. 安装 Breakpad 工具
    下载 minidump_stackwalk(Breakpad 工具集的一部分):

  3. 运行分析命令

    1
    minidump_stackwalk your_crash.dmp /path/to/symbols > crash_stack.txt

    输出的 crash_stack.txt 中会包含符号化后的调用栈。

方案 2:使用第三方服务(推荐,生产环境)

无需手动处理符号,直接上传 dump 文件即可自动分析:

  • Sentry:支持 Electron 崩溃上报,自动符号化调用栈(需配置符号文件上传)。
  • Backtrace:专门针对 Electron 优化,支持实时崩溃监控和深度分析。
  • BugSplat:提供 Electron SDK,一键集成崩溃报告和分析。
方案 3:使用 Electron 的 crashReporter 配合 Socorro(自建服务)

若需自建服务,可使用 Mozilla 的 Socorro(Electron 文档推荐),但部署复杂度较高,适合大型团队。

4. Web worker

以下是 Node.js 中 Web Workers(worker_threads 模块) 的完整示例,包含主线程与工作线程的通信、错误处理等核心功能。

一、核心概念

Node.js 的 worker_threads 模块允许在独立线程中执行 JavaScript 代码,避免 CPU 密集型任务阻塞主线程(事件循环)。

  • 主线程:创建 Worker、管理线程生命周期、与 Worker 通信。
  • 工作线程:执行耗时任务、通过消息传递与主线程交互。

二、完整示例

我们将实现一个「计算斐波那契数列」的场景(CPU 密集型任务),通过 Worker 避免阻塞主线程。

1. 项目结构
1
2
3
.
├── main.js # 主线程文件
└── worker.js # 工作线程文件
2. 主线程代码(main.js

负责创建 Worker、发送任务数据、接收结果并处理错误。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
const { Worker, isMainThread, parentPort } = require('worker_threads');
const path = require('path');

if (isMainThread) {
console.log('=== 主线程启动 ===');

// 1. 创建 Worker(指定工作线程文件路径)
const worker = new Worker(path.join(__dirname, 'worker.js'));

// 2. 向 Worker 发送任务(计算第 40 个斐波那契数)
const n = 40;
console.log(`主线程:发送任务,计算 fib(${n})`);
worker.postMessage(n);

// 3. 监听 Worker 返回的结果
worker.on('message', (result) => {
console.log(`主线程:收到结果,fib(${n}) = ${result}`);
worker.terminate(); // 任务完成后终止 Worker(可选,也可让 Worker 自行退出)
});

// 4. 监听 Worker 错误
worker.on('error', (err) => {
console.error('主线程:Worker 出错', err);
});

// 5. 监听 Worker 退出
worker.on('exit', (code) => {
console.log(`主线程:Worker 退出,退出码 ${code}`);
});

// 主线程继续执行其他任务(证明未被阻塞)
console.log('主线程:继续处理其他事务...');
}
3. 工作线程代码(worker.js

接收主线程任务、执行计算、返回结果。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
const { Worker, isMainThread, parentPort } = require('worker_threads');

if (!isMainThread) {
console.log('=== 工作线程启动 ===');

// 斐波那契计算函数(CPU 密集型)
function fib(n) {
if (n <= 1) return n;
return fib(n - 1) + fib(n - 2);
}

// 1. 监听主线程发送的消息
parentPort.on('message', (n) => {
console.log(`工作线程:收到任务,计算 fib(${n})`);

// 2. 执行计算
const result = fib(n);

// 3. 将结果发送回主线程
parentPort.postMessage(result);

// 4. 任务完成后,工作线程可自行退出(可选)
process.exit();
});
}

三、运行示例

  1. 确保已安装 Node.js(建议 v12+,worker_threads 从 v10.5.0 开始实验性支持,v12+ 稳定)。
  2. 在项目目录下执行:
    1
    node main.js
预期输出
1
2
3
4
5
6
7
=== 主线程启动 ===
主线程:发送任务,计算 fib(40)
主线程:继续处理其他事务...
=== 工作线程启动 ===
工作线程:收到任务,计算 fib(40)
主线程:收到结果,fib(40) = 102334155
主线程:Worker 退出,退出码 0

四、进阶用法:传递复杂数据与共享内存

1. 传递对象(自动序列化)

Worker 之间传递对象时,会通过 结构化克隆算法 序列化(类似 JSON.parse(JSON.stringify()),但支持更多类型,如 DateRegExp)。

1
2
3
4
5
6
7
8
// 主线程
worker.postMessage({ type: 'compute', data: [1, 2, 3] });

// 工作线程
parentPort.on('message', (msg) => {
console.log(msg.type); // 'compute'
console.log(msg.data); // [1, 2, 3]
});
2. 共享内存(SharedArrayBuffer

若需高效共享大量数据,可使用 SharedArrayBuffer(避免序列化开销),但需注意线程安全(配合 Atomics 操作)。

1
2
3
4
5
6
7
8
9
10
11
// 主线程
const sharedBuffer = new SharedArrayBuffer(4); // 4 字节(1 个 Int32)
const sharedArray = new Int32Array(sharedBuffer);
worker.postMessage({ sharedBuffer });

// 工作线程
parentPort.on('message', (msg) => {
const { sharedBuffer } = msg;
const sharedArray = new Int32Array(sharedBuffer);
Atomics.store(sharedArray, 0, 123); // 线程安全地写入
});

五、注意事项

  1. 不能直接共享变量:Worker 有独立的内存空间,只能通过消息传递或 SharedArrayBuffer 共享数据。
  2. 序列化开销:传递大对象时,结构化克隆会有性能损耗,此时优先考虑 SharedArrayBuffer
  3. Worker 数量限制:避免创建过多 Worker(建议不超过 CPU 核心数),否则线程切换开销会抵消收益。
  4. 模块支持:Worker 中可使用 require() 或 ESM import,但部分 Node.js API(如 cluster 模块)不可用。

Electron开发重点
https://cszy.top/20260212-Electron开发重点/
作者
csorz
发布于
2026年2月12日
许可协议