工作流程

主进程
- 可以看做是 package.json 中 main 属性对应的文件
- 一个应用只会有一个主进程
- 只有主进程可以进行GUI的API操作
渲染进程
- Windows 中显示的界面通过渲染进程表现
- 一个应用可以有多个渲染进程
环境搭建
可以使用 electron-vite 快速搭建一个项目
npm create @quick-start/electron@latest打包配置
electron-builder.yml中 nsis 的配置
nsis: artifactName: ${name}-${version}-setup.${ext} shortcutName: ${productName} uninstallDisplayName: ${productName} createDesktopShortcut: always # 始终创建桌面快捷方式 oneClick: false # 一键安装(直接安装,不显示界面) true=一键安装 false=手动安装 perMachine: false # 仅当前用户安装(true=所有用户) true=所有用户 false=当前用户 allowElevation: true # 允许提权(如需安装到 Program Files) true=允许 false=不允许 allowToChangeInstallationDirectory: true # 用户选择安装路径 true=允许 false=不允许 runAfterFinish: true # 安装完成后自动运行应用 true=允许 false=不允许 # silent: true # 完全静默(无任何界面,适合企业部署)开发
项目结构
约定
推荐使用如下项目结构
.├──src│ ├──main│ │ ├──index.ts│ │ └──...│ ├──preload│ │ ├──index.ts│ │ └──...│ └──renderer # with vue, react, etc.│ ├──src│ ├──index.html│ └──...├──electron.vite.config.ts├──package.json└──...遵循此约定,electron-vite 可以用最少的配置进行工作。
当运行 electron-vite 时,它会自动寻找主进程、渲染器和预加载脚本的入口文件。默认的入口配置:
- 主进程:
<root>/src/main/{index|main}.{js|ts|mjs|cjs} - 预加载脚本:
<root>/src/preload/{index|preload}.{js|ts|mjs|cjs} - 渲染器:
<root>/src/renderer/index.html
生命周期
主进程生命周期
- 准备阶段
- Electron 初始化
- 加载 Node.js 环境
- 应用启动
app.whenReady(): 应用准备就绪的 Promiseready事件: 当 Electron 完成初始化时触发
- 窗口管理
- 创建 BrowserWindow 实例
window-all-closed事件: 所有窗口关闭时触发before-quit事件: 应用开始退出前触发will-quit事件: 即将退出时触发quit事件: 应用退出时触发
渲染进程生命周期
- 页面加载
dom-ready: DOM 加载完成did-finish-load: 页面加载完成did-fail-load: 页面加载失败
- 页面交互
- 各种 DOM 事件和用户交互
- 页面关闭
beforeunload: 页面即将卸载unload: 页面卸载
常用生命周期事件
const { app, BrowserWindow } = require('electron')
app.whenReady().then(() => { // 应用已准备好,可以创建窗口 const win = new BrowserWindow({ /* 配置 */ })
win.webContents.on('did-finish-load', () => { console.log('页面加载完成') })})
app.on('window-all-closed', () => { if (process.platform !== 'darwin') { app.quit() }})
app.on('before-quit', (e) => { // 可以在这里阻止退出或执行清理操作})
app.on('will-quit', () => { // 即将退出})
app.on('quit', () => { // 应用已退出})function createWindow() { // 创建窗口 let mainWindow = new BrowserWindow({ x: 100, y: 100,//设置窗口显示的位置 width: 900,//窗口宽度 height: 670,//窗口高度 maxHeight:670,//窗口最大高度 minHeight:100,//窗口最小高度 maxWidth:1000,//窗口最大宽度 minWidth: 100,//窗口最小宽度 resizable: true,//窗口是否可以改变大小 true可以 false不可以 show: false, //默认情况下创建一个窗口对象之后就会显示,设置为false就不会显示 autoHideMenuBar: true,//菜单栏是否隐藏 true隐藏 false不隐藏 frame: false,//窗口是否显示边框 true显示 false不显示 title:'测试',//窗口标题 icon:'图标地址',//图标 ...(process.platform === 'linux' ? { icon } : {}), webPreferences: { preload: join(__dirname, '../preload/index.js'), sandbox: false } })}修改窗口标题时要先将 index.html中的title标签设置为空

模态窗口
主窗口创建新的窗口以后不允许操作主窗口
parent: BrowserWindow.fromWebContents(event.sender),//父窗口
modal: true,//是否为模态窗口 true是 false否
ipcMain.handle('open-new-window', (event, url) => { // 创建新窗口 const newWindow = new BrowserWindow({ width: 800, height: 600, show: false, parent: BrowserWindow.fromWebContents(event.sender),//父窗口 modal: true,//模态窗口 webPreferences: { preload: join(__dirname, '../preload/index.js'), // 使用绝对路径更安全 sandbox: true, // ✅ 启用沙盒 contextIsolation: true, // ✅ 启用上下文隔离 nodeIntegration: false // ✅ 禁用 Node 集成 } })
// 加载内容(使用参数传入的 URL 或默认页面) const loadUrl = url || 'about:blank' newWindow.loadURL(loadUrl).catch(err => { console.error('Failed to load window content:', err) newWindow.close() })
// 窗口准备就绪后显示 newWindow.on('ready-to-show', () => { newWindow.show() })
// 窗口关闭时清理引用 newWindow.on('closed', () => { // 在实际应用中可能需要执行其他清理操作 })
// 返回窗口 ID 以便后续管理 return newWindow.id})自定义菜单
label 菜单名称
submenu 子菜单
role 是一个特殊属性,用于指定菜单项的标准功能,而不需要手动实现这些功能的代码。
click点击事件
import { Menu } from 'electron'
//定义菜单模版let menuTemp = [ { label: '文件',//菜单名称 submenu: [ { label: '新建', click: () => { //点击事件 console.log('新建') } }, { type: 'separator' },//分隔符 { label: '打开' }, { label: '关于' ,role: 'about' }, { label: '最小化', role: 'minimize' } ] }, { label: '编辑' }]
//用上述模版生成菜单项let menu = Menu.buildFromTemplate(menuTemp)
//将自定义菜单项添加到应用Menu.setApplicationMenu(menu)获得当前平台
//获取当前平台console.log(process.platform)菜单角色及类型
accelerator 设置快捷键
icon设置图标
import { Menu } from 'electron'
//定义菜单模版let menuTemp = [ { label: '文件', submenu: [ { label: '复制', role: 'copy' }, { label: '粘贴', role: 'paste' }, { label: '剪贴', role: 'cut' }, { label: '全选', role: 'selectall' }, { label: '最小化', role: 'minimize' } ] }, { label: '类型', submenu: [ { label: '选项一', type: 'checkbox' }, { label: '选项二', type: 'checkbox' }, { label: '选项三', type: 'checkbox' }, { type: 'separator' }, { label: 'item1', type: 'radio' }, { label: 'item2', type: 'radio' }, { type: 'separator' }, { label: 'windows', type: 'submenu', role: 'windowMenu' } //window原生菜单 ] }, { label: '其他', submenu: [ { label: '打开', icon: './resources/icon.png', accelerator: 'ctrl + o',//设置快捷键 click: () => { console.log('操作完了') } }, ] }]
//利用上述模版生成菜单项let menu = Menu.buildFromTemplate(menuTemp)
//将自定义菜单项添加到应用Menu.setApplicationMenu(menu)动态创建菜单
页面
<input type="text" placeholder="请输入内容" v-model="lableValue" /> <button @click="newMenu">创建新菜单</button>
/////////////////////////////jsconst lableValue = ref('')
const newMenu = () => { window.electron1.newMenu('new-menu', lableValue.value)}preload
contextBridge.exposeInMainWorld('electronAPI', { newMenu: (menu, ) => ipcRenderer.invoke('new-menu', menu, test)})main
import {ipcMain, Menu, MenuItem } from 'electron'let menuList = Menu()
ipcMain.handle('new-menu', (event, menu, lableValue) => { console.log(lableValue) console.log(menu) let newMenuItem = new MenuItem({ label: lableValue, submenu: new Menu() }) menuList.append(newMenuItem) Menu.setApplicationMenu(menuList)})自定义右键菜单
main
import { BrowserWindow, ipcMain, Menu } from 'electron'//定义鼠标右键菜单内容let contextTemp = [ { label: '复制', role: 'copy', accelerator: 'ctrl + c' }, { label: '粘贴', role: 'paste', accelerator: 'ctrl + v' }, { label: '剪贴', role: 'cut', accelerator: 'ctrl + x' }, { label: '全选' }, { type: 'separator' }, { label: '其他', click: () => { console.log('其他') } }]//依据上述的内容创建 menulet menuRight = Menu.buildFromTemplate(contextTemp)
//给鼠标右键添加监听ipcMain.handle('right-click', (event) => { //获取当前窗口 let win = BrowserWindow.fromWebContents(event.sender) //设置右键菜单 win.webContents.on('context-menu', (e, params) => { //设置菜单位置 menuRight.popup({ window: win, x: params.x, y: params.y }) })})preload
contextBridge.exposeInMainWorld('electronAPI', { rightClick: () => ipcRenderer.invoke('right-click')})页面
//给右键添加监听window.electronAPI.rightClick('right-click', (event, data) => { console.log('右键菜单', data)})主进程与渲染进程通信
ipcMain.on() 和 ipcMain.handle() 是两种不同的 IPC 通信方式
IPC通信
ipcMain.on()
ipcMain.on() 与 event.reply()
main
ipcMain.on('message-from-renderer', (event, arg) => { console.log(arg); // 打印消息 event.reply('message-reply', 'pong'); // 回复消息});preload
const { contextBridge, ipcRenderer } = require('electron');contextBridge.exposeInMainWorld('electronAPI', { send: (channel, data) => ipcRenderer.send(channel, data), invoke: (channel, data) => ipcRenderer.invoke(channel, data), on: (channel, callback) => ipcRenderer.on(channel, callback)});渲染进程
// 渲染进程调用window.electronAPI.invoke('get-data').then(result => { ... });特点:
- 单向通信:渲染进程通过
ipcRenderer.send()发送消息,主进程通过event.reply()回复。 - 多次响应:主进程可以多次调用
event.reply()发送多条回复。 - 无 Promise 支持:渲染进程需要通过监听另一个事件来接收回复。
- 适用场景:适合需要主进程主动推送多次响应的场景(如进度更新、实时数据流)。
ipcMain.handle()
main
ipcMain.handle('message-from-renderer', (event, arg) => { console.log(arg); // 打印消息 return 'pong'; // 直接返回值});渲染进程
const { ipcRenderer } = require('electron');
// 使用 invoke 调用并等待结果(async () => { const result = await ipcRenderer.invoke('message-from-renderer', 'ping'); console.log(result); // 'pong'})();特点:
- 双向通信:渲染进程通过
ipcRenderer.invoke()调用,主进程直接返回结果(类似函数调用)。 - Promise 支持:渲染进程通过
await或.then()直接获取返回值。 - 单次响应:主进程只能通过
return返回一次结果。 - 适用场景:适合需要同步等待结果的场景(如读取文件、数据库查询)。
webContents.send()
使用 SharedArrayBuffer 和 Atomics 实现高性能数据共享(适用于大量数据交换)。
main
// 创建共享内存const sharedBuffer = new SharedArrayBuffer(1024);const sharedArray = new Uint8Array(sharedBuffer);
// 写入数据sharedArray[0] = 42;
// 通过 IPC 传递 SharedArrayBuffer(注意安全限制)ipcRenderer.send('share-buffer', sharedBuffer);注意事项
- 需要启用
contextIsolation: false或通过postMessage传递。 - 需配置 CSP 头允许
SharedArrayBuffer(如Cross-Origin-Opener-Policy: same-origin)
使用 Node.js 的 child_process
主进程可以创建子进程(如 Python、C++ 程序),并通过标准输入输出或 IPC 通信。
main
const { exec } = require('child_process');
const pythonProcess = exec('python script.py', (error, stdout, stderr) => { if (error) console.error(error); console.log(stdout); // 子进程输出});
// 发送数据到子进程pythonProcess.stdin.write('data\n');适用场景
- 需要调用外部程序或脚本。
- 高性能计算(如机器学习、图像处理)。
使用 WebSocket 或 HTTP 通信
如果应用涉及多个 Electron 实例或远程服务,可以用 WebSocket/HTTP 通信。
主进程或渲染进程:
const ws = new WebSocket('ws://localhost:8080');ws.onmessage = (event) => { console.log('Received:', event.data);};ws.send('Hello Server!');适用场景
- 多窗口/多进程通信。
- 与后端服务或其他设备交互。
使用文件或数据库作为中介
通过读写文件或数据库(如 SQLite、LevelDB)实现数据交换。
使用 MessagePort (高级 API)
适用性:官方推荐替代 remote 的方案,支持复杂通信场景
// 主进程const { port1, port2 } = new MessageChannel();win.webContents.postMessage('port', null, [port1]);
// 渲染进程(通过 preload 暴露)window.electronAPI.onPortMessage((port) => { port.postMessage('ping'); port.onmessage = (e) => console.log(e.data);});注意
- 上下文隔离(Context Isolation):
- 默认启用,必须通过
preload脚本使用contextBridge暴露 API。 - 错误配置会导致
require或window.electronAPI不可用。
- 默认启用,必须通过
- 沙箱(Sandbox):
- 渲染进程默认受限,禁止直接访问 Node.js API。
- 需通过
preload脚本的白名单控制权限。
渲染进程间的通信
在 Electron 中,渲染进程(Renderer Process)之间不能直接通信,必须通过主进程(Main Process)作为中介。
通过主进程转发
main
const { ipcMain } = require('electron');
// 存储所有窗口的引用const windows = new Map();
// 注册转发逻辑ipcMain.on('renderer-to-renderer', (event, { targetWindowId, message }) => { const targetWindow = windows.get(targetWindowId); if (targetWindow) { targetWindow.webContents.send('renderer-message', message); }});渲染进程A (发送消息):
const { ipcRenderer } = require('electron');
// 发送给ID为2的窗口ipcRenderer.send('renderer-to-renderer', { targetWindowId: 2, message: 'Hello from Window 1'});渲染进程B (接收消息):
ipcRenderer.on('renderer-message', (event, message) => { console.log('Received:', message); // "Hello from Window 1"});使用 MessagePort (Electron 14+)
原理:
- 通过主进程建立直接的
MessageChannel连接 - 高性能,适合频繁通信
main
const { MessageChannel } = require('electron');
// 窗口1和窗口2建立时连接它们function connectWindows(win1, win2) { const { port1, port2 } = new MessageChannel();
win1.webContents.postMessage('port', null, [port1]); win2.webContents.postMessage('port', null, [port2]);}preload.js
contextBridge.exposeInMainWorld('electronAPI', { onPort: (callback) => { window.addEventListener('message', (event) => { if (event.data === 'port') { const [port] = event.ports; callback(port); } }); }});渲染进程A (发送端):
window.electronAPI.onPort((port) => { port.postMessage('Direct message!');});渲染进程B (接收端):
window.electronAPI.onPort((port) => { port.onmessage = (e) => { console.log('Direct message:', e.data); };});使用 localStorage/sessionStorage (同源窗口)
适用场景:
- 仅限同一浏览器窗口内的多个页面(如多个
<webview>) - 简单状态同步
页面
// 窗口A发送localStorage.setItem('shared-data', JSON.stringify({ key: 'value' }));
// 窗口B监听window.addEventListener('storage', (e) => { if (e.key === 'shared-data') { console.log('Data changed:', JSON.parse(e.newValue)); }});使用远程共享状态
适用场景:
- 需要跨窗口实时同步复杂数据
- 例如使用
Redux/MobX等状态管理库
main
const sharedStore = createStore(/* reducers */);
// 将store注入所有窗口function attachStoreToWindow(win) { win.sharedStore = sharedStore;}preload
contextBridge.exposeInMainWorld('electronAPI', { getSharedStore: () => window.sharedStore});如何选择
- 大多数场景:方案1(主进程转发)
- 高性能需求:方案2(MessagePort)
- 同源简单同步:方案3(localStorage)
- 复杂状态管理:方案4(共享状态库)
dialog模块
dialog 模块用于显示原生系统对话框,如文件选择、消息提示、错误弹窗等。它提供了与操作系统风格一致的对话框,增强了应用的原生体验。
1. 基本用法
dialog 需要在 主进程 中使用,但可以通过 ipc 在渲染进程中调用。
1.1 引入模块
// 主进程中const { dialog } = require('electron');1.2 常用方法
| 方法 | 用途 | 返回值 |
|---|---|---|
dialog.showOpenDialog([options]) | 打开文件/目录选择对话框 | Promise<{ filePaths: string[] }> |
dialog.showSaveDialog([options]) | 显示文件保存对话框 | Promise<{ filePath: string }> |
dialog.showMessageBox([options]) | 显示消息提示框(确认/取消) | Promise<{ response: number }> |
dialog.showErrorBox(title, content) | 显示错误弹窗(无回调) | void |
2. 文件选择对话框 (showOpenDialog)
const { dialog } = require('electron');
// 异步方式(推荐)dialog.showOpenDialog({ title: '选择文件', properties: ['openFile', 'multiSelections'], // 可选文件、多选 filters: [ { name: 'Images', extensions: ['jpg', 'png'] }, { name: 'All Files', extensions: ['*'] } ]}).then(result => { if (!result.canceled) { console.log('选择的文件:', result.filePaths); }});常用选项 (options):
properties(属性):openFile(选择文件)openDirectory(选择目录)multiSelections(允许多选)
filters: 文件类型过滤器
3. 保存文件对话框 (showSaveDialog)
dialog.showSaveDialog({ title: '保存文件', defaultPath: '/path/to/default.txt', filters: [{ name: 'Text', extensions: ['txt'] }]}).then(result => { if (!result.canceled) { fs.writeFileSync(result.filePath, '文件内容'); }});4. 消息提示框 (showMessageBox)
dialog.showMessageBox({ type: 'info', title: '提示', message: '操作确认', detail: '确定要删除吗?', buttons: ['确定', '取消'], cancelId: 1 // 按ESC时返回的按钮索引}).then(result => { if (result.response === 0) { console.log('用户点击了确定'); }});按钮返回值:
result.response是按钮的索引(从0开始)
5. 错误弹窗 (showErrorBox)
dialog.showErrorBox( '错误标题', '发生了一个致命错误!');// 无回调,仅显示6. 在渲染进程中调用 dialog
由于安全限制,渲染进程不能直接使用 dialog,需通过 ipc 通信。
6.1 主进程暴露 API (preload.js)
const { contextBridge, ipcRenderer } = require('electron');
contextBridge.exposeInMainWorld('electronAPI', { openFile: () => ipcRenderer.invoke('dialog:openFile')});6.2 主进程实现 (main.js)
ipcMain.handle('dialog:openFile', async () => { const result = await dialog.showOpenDialog({ /* 选项 */ }); return result.filePaths;});6.3 渲染进程调用
document.getElementById('open-btn').addEventListener('click', async () => { const files = await window.electronAPI.openFile(); console.log('选择的文件:', files);});7. 跨平台差异
| 功能 | Windows | macOS | Linux |
|---|---|---|---|
| 文件选择器 | √ | √ | √ (风格可能不同) |
| 消息框图标 | ⚠️ 部分样式不同 | ✅ | ⚠️ 依赖桌面环境 |
| 默认路径 | 支持 C:\\ | 支持 /Users | 支持 /home |
8. 最佳实践
-
错误处理:
try {const result = await dialog.showOpenDialog();} catch (err) {console.error('对话框错误:', err);} -
优化用户体验
- 设置合理的
defaultPath(如上次打开的目录) - 使用
filters限制可选择的文件类型
- 设置合理的
-
安全提示:
const isValid = fs.existsSync(userSelectedPath);
消息通知
通过前端实现
const createNotification = () => { let option = { title: '标题', body: '内容', icon: './assets/electron.svg' }
//创建消息卡 let myNotification = new Notification(option.title, option)
//监听是否点击 myNotification.onclick = () => { console.log('点击了消息卡') }}
<a target="_blank" rel="noreferrer" @click="createNotification">消息通知</a>快捷键注册
剪切板模块
sqlite数据库操作
控制台输出乱码
chcp 65001