3663 字
18 分钟
electron入门

工作流程#

image-20250426223439998

主进程#

  • 可以看做是 package.json 中 main 属性对应的文件
  • 一个应用只会有一个主进程
  • 只有主进程可以进行GUI的API操作

渲染进程#

  • Windows 中显示的界面通过渲染进程表现
  • 一个应用可以有多个渲染进程

环境搭建#

可以使用 electron-vite 快速搭建一个项目

npm create @quick-start/electron@latest

electron-vite (cn-evite.netlify.app)

打包配置#

electron-builder.ymlnsis 的配置

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

生命周期#

主进程生命周期#

  1. 准备阶段
    • Electron 初始化
    • 加载 Node.js 环境
  2. 应用启动
    • app.whenReady(): 应用准备就绪的 Promise
    • ready 事件: 当 Electron 完成初始化时触发
  3. 窗口管理
    • 创建 BrowserWindow 实例
    • window-all-closed 事件: 所有窗口关闭时触发
    • before-quit 事件: 应用开始退出前触发
    • will-quit 事件: 即将退出时触发
    • quit 事件: 应用退出时触发

渲染进程生命周期#

  1. 页面加载
    • dom-ready: DOM 加载完成
    • did-finish-load: 页面加载完成
    • did-fail-load: 页面加载失败
  2. 页面交互
    • 各种 DOM 事件和用户交互
  3. 页面关闭
    • 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标签设置为空

image-20250429165220697

模态窗口#

主窗口创建新的窗口以后不允许操作主窗口

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>
/////////////////////////////js
const 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('其他')
}
}
]
//依据上述的内容创建 menu
let 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()#

使用 SharedArrayBufferAtomics 实现高性能数据共享(适用于大量数据交换)。

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);
});

注意#

  1. 上下文隔离(Context Isolation)
    • 默认启用,必须通过 preload 脚本使用 contextBridge 暴露 API。
    • 错误配置会导致 requirewindow.electronAPI 不可用。
  2. 沙箱(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. 跨平台差异#

功能WindowsmacOSLinux
文件选择器√ (风格可能不同)
消息框图标⚠️ 部分样式不同⚠️ 依赖桌面环境
默认路径支持 C:\\支持 /Users支持 /home

8. 最佳实践#

  1. 错误处理

    try {
    const result = await dialog.showOpenDialog();
    } catch (err) {
    console.error('对话框错误:', err);
    }
  2. 优化用户体验

    • 设置合理的 defaultPath(如上次打开的目录)
    • 使用 filters 限制可选择的文件类型
  3. 安全提示

    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数据库操作#

控制台输出乱码#

Terminal window
chcp 65001
electron入门
https://fuwari.vercel.app/posts/electron/electron入门/
作者
zhouyeshan
发布于
2025-05-05
许可协议
CC BY-NC-SA 4.0