多端开发之桌面端应用之Electron
前言:从 Web 到桌面,多端统一的终点
在现代软件开发体系中,多端架构已成为主流选择。从 Web、Mobile、MiniApp 到 Desktop,开发者需要在不同平台间平衡开发效率与用户体验。桌面端作为这个体系中的关键一环,承载着高性能计算、专业工具、企业办公等核心场景。
桌面端在多端体系中的价值
桌面端在多端应用体系中(包括Web端、Mobile端、小程序端等)占据独特地位,运行于Windows、macOS、Linux等操作系统。它拥有更强的系统权限、更好的性能、更丰富的原生能力,非常适合专业的工具场景。
桌面端的独特优势:
- 系统级权限:可以访问文件系统、管理进程、发送系统通知。
- 性能优势:不受浏览器沙箱限制,能够充分利用硬件资源。
- 专业场景:适用于开发IDE、设计工具、音视频处理、数据分析等复杂应用。
- 企业级应用:满足内网部署、安全隔离、深度定制化等需求。
为什么 Electron 能打破前端与桌面的边界
传统桌面开发需要掌握 C++、C#、Objective-C 等原生语言,学习曲线陡峭。而 Electron 的出现,让前端开发者能够使用熟悉的 JavaScript/TypeScript、HTML、CSS 技术栈构建桌面应用,极大地降低了开发门槛。
其核心代码示例如下:
const { app, BrowserWindow } = require('electron');
// 创建应用窗口
const win = new BrowserWindow({
width: 1200,
height: 800,
webPreferences: {
nodeIntegration: false,
contextIsolation: true
}
});
// 加载前端应用页面
win.loadFile('index.html');
Electron 的突破性创新:
- 技术栈统一:直接使用前端技术。
- 生态复用:可以接入庞大的 npm 生态。
- 快速迭代:完整的前端工程化工具链支持。
- 跨平台一致:一套代码可构建 Windows、macOS、Linux 应用。
前端工程师的桌面新边界:从浏览器到操作系统
Electron 将前端开发者的能力边界从浏览器扩展到了操作系统层面。对比浏览器中的 Web 应用,Electron 应用拥有完整文件系统访问、进程管理、完整系统通知 API、不受 CORS 限制的网络请求、多窗口与托盘控制等能力,并且性能更接近原生应用,支持完全离线运行。
趋势视角:WebRuntime 的崛起
我们正见证着 WebRuntime 的演进:从第一代的浏览器,到第二代的 Node.js(服务端 JavaScript),再到第三代的 Electron、Tauri(桌面 WebRuntime),以及第四代的 WebContainer、Deno(云端 WebRuntime)。Electron 正是第三代的核心代表,它融合了 Web 标准、Node.js 运行时和操作系统原生 API。
Electron 如何成为「多端统一」的关键拼图
在多端开发体系中,Electron 扮演着桌面端容器的角色。开发者可以通过适配器模式,实现核心业务逻辑的跨平台复用(Web、Electron、React Native),同时利用 Electron 的特有能力增强桌面端体验。UI层(如 React/Vue 组件)和业务逻辑层可以高度共享,通过统一的构建工具进行打包。
Electron 的起源与发展
起源:从 Atom Shell 到 Electron
Electron 起源于 2013 年,由 GitHub 开发,最初名为 Atom Shell,用于构建其 Atom 文本编辑器。GitHub 需要一款跨平台的桌面编辑器,并希望使用 Web 技术进行快速迭代,同时能访问系统级 API。其核心创新在于将 Chromium 浏览器内核与 Node.js 运行时融合。
2015 年,项目正式开源并更名为 Electron,迅速获得社区关注。GitHub 意识到它不只是一个编辑器框架,而是一个通用的桌面应用开发平台。
- 2013年:Atom Shell 诞生,作为 GitHub 内部项目。
- 2015年:更名为 Electron 并正式开源。
- 2016年:Electron 1.0 发布,API 进入稳定阶段。
- 2018年:Electron 3.0 引入重要的上下文隔离安全特性。
- 2020年至今:持续迭代,默认启用上下文隔离,并不断优化性能与安全,支持最新 Web 标准。
核心技术栈:Chromium + Node.js + V8
Electron 应用的本质是一个集成了 Node.js 的 Chromium 浏览器。其架构主要包含三部分:
- Chromium:用于渲染界面,包含 Blink 渲染引擎、V8 JavaScript 引擎和 Skia 图形库,采用多进程架构保障稳定性。
- Node.js:提供底层系统能力,基于 libuv 事件循环,提供文件系统、网络等 API。
- Native APIs:提供跨平台抽象,封装了窗口管理、系统托盘、全局快捷键、原生对话框等操作系统原生功能。
例如,Electron 28 包含了 Chromium 120、Node.js 18 和 V8 12。
工程视角:版本演进与构建优化
Electron 需要协调 Chromium、Node.js 和自身三个独立项目的版本,这带来了挑战。在 package.json 中,electron 依赖项就包含了特定的 Chromium 和 Node.js 版本。你可以通过 process.versions 查看具体信息。
构建优化策略包括:
- 原生模块重编译:为 Electron 的 Node 版本重新编译原生模块(使用
electron-rebuild)。 - 多版本兼容:在代码中检测运行环境(
process.versions.electron),区分 Electron 和纯浏览器环境。
趋势视角:Electron 与竞争方案的格局
桌面开发方案众多,各有千秋。Qt (C++), WPF (C#) 是传统原生方案。NW.js 是老牌的 Web 技术方案。Electron 是目前最成熟、生态最丰富的 Web 技术方案。React Native Desktop、Flutter Desktop 是移动优先的跨端方案。Tauri (Rust + Web) 是新兴的轻量级方案,体积小性能好。WebContainer 则是纯粹的 Web 沙盒环境。
就市场份额而言,Electron 最为成熟,占有率最高;Tauri 作为挑战者增长迅速;Flutter Desktop 仍在发展中;NW.js 则逐渐被取代。
生态现状与代表应用
Electron 拥有庞大的生态和众多成功案例:
- 开发工具:Visual Studio Code、Atom、Postman、GitHub Desktop。
- 协作办公:Slack、Discord、Microsoft Teams、Notion。
- 设计创意:Figma Desktop、Obsidian、Typora。
- 其他工具:Twitch、WhatsApp Desktop、1Password 早期版本。
其生态规模庞大,在 npm 上有超过 15,000 个相关包,在 GitHub 上拥有超过 110,000 星,被数万款生产环境应用所使用。
核心概念解析
Electron 的主进程和渲染进程有什么区别?如何进行通信?
主进程(Main Process):
- 每个 Electron 应用有且仅有一个主进程。
- 运行在 Node.js 环境中,可使用所有 Node.js API。
- 负责应用生命周期(启动、退出)、创建管理窗口(BrowserWindow)、处理系统级操作(菜单、托盘、通知)。
渲染进程(Renderer Process):
- 每个 BrowserWindow 实例对应一个独立的渲染进程。
- 运行在 Chromium 环境中,类似于浏览器标签页,负责展示 Web 界面和用户交互。
- 默认无法直接访问 Node.js API(出于安全考虑)。
进程间通信(IPC)方式:
- IPC 模块:使用
ipcMain(主进程)和ipcRenderer(渲染进程)模块发送和监听消息。 - Preload 脚本:在渲染进程加载前注入,作为安全桥梁连接两个进程。
- Remote 模块:已废弃,不推荐使用。
通信时,主进程通过 ipcMain.on 或 ipcMain.handle 监听,渲染进程通过 ipcRenderer.send 或 ipcRenderer.invoke 发送,并通过 contextBridge 在预加载脚本中安全暴露 API。
在 Electron 中,如何优化应用性能?
启动性能优化:
- 延迟加载:按需加载非必要模块。
- V8 快照:使用
mksnapshot预编译常用代码。 - 窗口预加载:提前创建隐藏窗口,需要时快速显示。
运行时性能优化:
- 进程隔离:避免主进程阻塞,将耗时任务放入独立进程或 Worker。
- 渲染优化:启用硬件加速,使用 CSS
transform/opacity做动画,对长列表进行虚拟滚动。 - 内存管理:及时销毁不用的
BrowserWindow,监控并避免内存泄漏。
打包体积优化:
- Tree Shaking 与代码分割。
- 使用
electron-builder的asarUnpack合理排除大文件。 - 仅打包目标平台所需的原生模块依赖。
Electron 项目初始化
项目初始化主要有两种推荐方案:官方的 Electron Forge 和社区的 Electron Vite。
基于 Electron Forge(官方推荐)
Electron Forge 是官方维护的完整工具链,覆盖创建、开发、打包、发布全流程,适合生产环境。
创建项目:
# 使用 npm(也可用 yarn/pnpm)
npm init electron-app@latest my-electron-app
cd my-electron-app
国内用户可配置 npm 镜像源加速。
使用模板:Forge 提供了多种模板,如 webpack、webpack-typescript、vite、vite-typescript。其中 vite-typescript 模板开发体验最佳。
# 使用 Vite + TypeScript 模板
npm init electron-app@latest my-app -- --template=vite-typescript
创建后的项目包含主进程、预加载脚本、渲染进程代码及配置文件。
集成前端框架(以 Vue 3 为例):
- 安装 Vue 及相关 Vite 插件。
- 修改渲染器的 Vite 配置(
vite.renderer.config.js),添加vue()插件和路径别名。 - 修改
index.html,提供挂载节点。 - 创建
App.vue组件,并通过预加载脚本暴露的 API(如window.electronAPI)调用 Electron 能力。 - 修改渲染器入口文件(
renderer.js),使用createApp挂载 Vue 应用。 - 在预加载脚本(
preload.js)中使用contextBridge.exposeInMainWorld安全暴露 API。 - 在主进程(
main.js)中添加对应的 IPC 处理器(如ipcMain.handle('ping', ...))。
启动应用:
npm start:启动开发模式(支持热更新)。npm run package:打包应用。npm run make:制作安装包。npm run publish:发布应用。
基于 Electron Vite
Electron Vite 是社区提供的轻量级方案,专为 Vite 优化,开发体验更快捷。
创建项目:
pnpm create @quick-start/electron my-app
创建时可选择原生 JavaScript 或 Vue、React、Svelte 等框架模板。项目结构清晰,主进程、预加载脚本、渲染进程代码分离。
其核心配置文件 electron.vite.config.ts 分别定义了主进程、预加载脚本和渲染进程的构建配置。
启动与构建:
npm run dev:开发模式。npm run build:构建应用。npm run build:win/mac/linux:打包特定平台安装包(通常需集成electron-builder)。
方案对比与选择建议
| 对比项 | Electron Forge | Electron Vite |
|---|---|---|
| 官方支持 | 官方维护 | 社区维护 |
| 开箱即用 | 完整工具链 | 需额外配置打包 |
| 构建速度 | 较慢 (Webpack) | 极快 (Vite) |
| 热更新 | 中等 | 毫秒级 |
| 打包发布 | 内置完整支持 | 需集成其他工具 |
| 适用场景 | 企业级项目、需要稳定支持 | 快速原型、现代前端框架、追求开发体验 |
选择建议:
- 需要稳定、完整工具链支持的企业级项目,选 Electron Forge。
- 追求极速开发体验、使用现代前端框架的快速原型项目,选 Electron Vite。
主进程与渲染进程深度解析
Electron 采用多进程架构。主进程负责应用生命周期和系统级操作,运行在 Node.js 环境。渲染进程负责界面展示,每个窗口一个进程,运行在 Chromium 环境。预加载脚本作为桥梁,运行在独立的上下文中,通过 contextBridge 安全地暴露特定 API 给渲染进程。
理解上下文隔离(Context Isolation)
上下文隔离是 Electron 的重要安全特性(从 Electron 12 开始默认启用),它确保预加载脚本和渲染进程运行在不同的 JavaScript 上下文中。这可以防止预加载脚本污染渲染进程全局对象,有效防御 XSS 攻击,遵循最小权限原则。
需要在创建 BrowserWindow 时配置 webPreferences 来启用:
const win = new BrowserWindow({
webPreferences: {
contextIsolation: true, // 启用上下文隔离
nodeIntegration: false, // 禁用 Node.js 集成(推荐)
preload: path.join(__dirname, 'preload.js') // 指定预加载脚本
}
});
预加载脚本应使用 contextBridge 来暴露 API:
const { contextBridge, ipcRenderer } = require('electron');
contextBridge.exposeInMainWorld('electronAPI', {
openFile: () => ipcRenderer.invoke('dialog:openFile'),
onUpdateAvailable: (callback) => {
const subscription = (event, ...args) => callback(...args);
ipcRenderer.on('update-available', subscription);
return () => ipcRenderer.removeListener('update-available', subscription);
}
});
进程间通信(IPC)模式
IPC 是 Electron 应用的核心机制,通过 ipcMain 和 ipcRenderer 进行异步、安全的数据交换。
模式一:渲染进程到主进程(单向)
适用于发送通知、记录日志等无需返回值的场景。
- 主进程:使用
ipcMain.on('event-name', handler)监听。 - 预加载脚本:暴露封装好的
ipcRenderer.send方法。 - 渲染进程:调用暴露的 API,如
window.electronAPI.logUserAction(...)。
模式二:渲染进程到主进程(双向)
最常用,适用于文件操作、数据库查询等需要返回结果的场景。
- 主进程:使用
ipcMain.handle('channel', async handler)处理请求并返回 Promise。 - 预加载脚本:暴露封装好的
ipcRenderer.invoke方法。 - 渲染进程:使用
await window.electronAPI.someMethod(...)调用并等待结果。
模式三:主进程到渲染进程
适用于主动通知渲染进程,如系统事件、后台任务进度更新。
- 主进程:通过
win.webContents.send('event', data)向特定窗口发送消息,或遍历BrowserWindow.getAllWindows()广播。 - 预加载脚本:暴露封装好的
ipcRenderer.on监听器,并返回取消订阅的函数。 - 渲染进程:调用监听函数,并在组件卸载时取消订阅以防止内存泄漏。
常用 API 详解
1. app 模块
控制应用生命周期,仅用于主进程。
- 核心事件:
whenReady(应用就绪)、window-all-closed(所有窗口关闭)、before-quit(退出前,可阻止)、activate(macOS 激活)。 - 常用方法:
app.getPath('userData'):获取用户数据目录等重要路径。app.setLoginItemSettings():设置开机自启。app.quit()/app.exit():退出应用。app.requestSingleInstanceLock():实现单实例应用(防止多开)。
2. BrowserWindow 模块
用于创建和管理应用窗口。
- 创建窗口:
new BrowserWindow(options),关键配置包括尺寸、是否显示边框 (frame)、标题栏样式 (titleBarStyle),以及重要的安全配置webPreferences(如contextIsolation,preload脚本路径)。 - 窗口事件:
ready-to-show(可显示)、close(关闭前,可阻止)、closed(已关闭)、maximize/unmaximize等。 - 窗口操作:
show()/hide()、minimize()/maximize()、setBounds()/setPosition()、setAlwaysOnTop()、setProgressBar()(设置任务栏进度条)、close()。 - 无边框窗口:设置
frame: false可创建自定义标题栏的窗口,需自行实现窗口控制逻辑。
contextBridge.exposeInMainWorld('windowControls', {
minimize: () => ipcRenderer.send('window:minimize'),
maximize: () => ipcRenderer.send('window:maximize'),
close: () => ipcRenderer.send('window:close')
});
// 主进程处理窗口控制
ipcMain.on('window:minimize', (event) => {
const win = BrowserWindow.fromWebContents(event.sender);
win.minimize();
});
ipcMain.on('window:maximize', (event) => {
const win = BrowserWindow.fromWebContents(event.sender);
if (win.isMaximized()) {
win.unmaximize();
} else {
win.maximize();
}
});
ipcMain.on('window:close', (event) => {
const win = BrowserWindow.fromWebContents(event.sender);
win.close();
});
3. ipcMain 和 ipcRenderer 模块高级用法
除了基础的进程间通信,Electron 的 IPC 模块还支持更高级的通信模式,如消息端口和流式数据传输。
消息端口通信提供了更灵活的双向通信能力。在主进程中,可以创建 MessageChannelMain,将端口发送给渲染进程:
// 主进程 - 使用 MessagePort 进行更灵活的通信
const { MessageChannelMain } = require('electron');
ipcMain.on('request-port', (event) => {
const { port1, port2 } = new MessageChannelMain();
// 监听端口 1 的消息
port1.on('message', (event) => {
console.log('收到消息:', event.data);
});
// 发送消息到端口 1
port1.postMessage({ type: 'greeting', data: 'Hello' });
// 将端口 2 发送到渲染进程
event.sender.postMessage('provide-port', null, [port2]);
// 启动端口
port1.start();
});
// 渲染进程(通过预加载脚本)
ipcRenderer.on('provide-port', (event) => {
const port = event.ports[0];
port.onmessage = (event) => {
console.log('收到主进程消息:', event.data);
};
port.postMessage({ type: 'response', data: 'Hi' });
port.start();
});
流式数据传输适用于处理大文件等场景,可以分块传输数据并报告进度:
// 主进程 - 处理大文件读取,流式传输
ipcMain.handle('file:readStream', async (event, filePath) => {
const fs = require('fs');
const stream = fs.createReadStream(filePath, { highWaterMark: 64 * 1024 });
let totalSize = 0;
const fileStats = await fs.promises.stat(filePath);
stream.on('data', (chunk) => {
totalSize += chunk.length;
const progress = (totalSize / fileStats.size) * 100;
// 发送进度更新
event.sender.send('file:readProgress', {
progress: progress.toFixed(2),
chunk: chunk.toString('base64') // 传输二进制数据
});
});
return new Promise((resolve, reject) => {
stream.on('end', () => resolve({ success: true }));
stream.on('error', (error) => reject(error));
});
});
4. Menu 模块
Menu 模块用于创建应用菜单和上下文菜单,是桌面应用的重要组成部分。
应用菜单定义了应用顶部的菜单栏。以下是一个包含文件、编辑、视图、窗口和帮助菜单的完整示例,并针对 macOS 平台做了特殊处理:
const { app, Menu, BrowserWindow, dialog } = require('electron');
// 定义菜单模板
const template = [
{
label: '文件',
submenu: [
{
label: '新建',
accelerator: 'CmdOrCtrl+N',
click: (menuItem, browserWindow, event) => {
console.log('新建文件');
}
},
{
label: '打开',
accelerator: 'CmdOrCtrl+O',
click: async () => {
const result = await dialog.showOpenDialog({
properties: ['openFile']
});
if (!result.canceled) {
console.log('打开文件:', result.filePaths[0]);
}
}
},
{ type: 'separator' },
{
label: '保存',
accelerator: 'CmdOrCtrl+S',
enabled: true,
click: () => {
// 保存文件逻辑
}
},
{
label: '另存为...',
accelerator: 'CmdOrCtrl+Shift+S'
},
{ type: 'separator' },
{
label: '退出',
accelerator: 'CmdOrCtrl+Q',
role: 'quit'
}
]
},
{
label: '编辑',
submenu: [
{ label: '撤销', accelerator: 'CmdOrCtrl+Z', role: 'undo' },
{ label: '重做', accelerator: 'CmdOrCtrl+Y', role: 'redo' },
{ type: 'separator' },
{ label: '剪切', accelerator: 'CmdOrCtrl+X', role: 'cut' },
{ label: '复制', accelerator: 'CmdOrCtrl+C', role: 'copy' },
{ label: '粘贴', accelerator: 'CmdOrCtrl+V', role: 'paste' },
{ label: '全选', accelerator: 'CmdOrCtrl+A', role: 'selectAll' }
]
},
{
label: '视图',
submenu: [
{ label: '重新加载', accelerator: 'CmdOrCtrl+R', role: 'reload' },
{ label: '强制重新加载', accelerator: 'CmdOrCtrl+Shift+R', role: 'forceReload' },
{ label: '切换开发者工具', accelerator: 'Alt+CmdOrCtrl+I', role: 'toggleDevTools' },
{ type: 'separator' },
{ label: '实际大小', accelerator: 'CmdOrCtrl+0', role: 'resetZoom' },
{ label: '放大', accelerator: 'CmdOrCtrl+Plus', role: 'zoomIn' },
{ label: '缩小', accelerator: 'CmdOrCtrl+-', role: 'zoomOut' },
{ type: 'separator' },
{ label: '全屏', accelerator: 'F11', role: 'togglefullscreen' }
]
},
{
label: '窗口',
role: 'window',
submenu: [
{ label: '最小化', accelerator: 'CmdOrCtrl+M', role: 'minimize' },
{ label: '关闭', accelerator: 'CmdOrCtrl+W', role: 'close' }
]
},
{
label: '帮助',
role: 'help',
submenu: [
{
label: '了解更多',
click: async () => {
const { shell } = require('electron');
await shell.openExternal('https://electronjs.org');
}
},
{ type: 'separator' },
{
label: '关于',
click: () => {
dialog.showMessageBox({
type: 'info',
title: '关于',
message: '我的应用',
detail: `版本: ${app.getVersion()}\nElectron: ${process.versions.electron}\nChrome: ${process.versions.chrome}\nNode.js: ${process.versions.node}`
});
}
}
]
}
];
// macOS 特殊处理
if (process.platform === 'darwin') {
template.unshift({
label: app.name,
submenu: [
{ label: `关于 ${app.name}`, role: 'about' },
{ type: 'separator' },
{ label: '服务', role: 'services' },
{ type: 'separator' },
{ label: `隐藏 ${app.name}`, accelerator: 'Command+H', role: 'hide' },
{ label: '隐藏其他', accelerator: 'Command+Alt+H', role: 'hideOthers' },
{ label: '显示全部', role: 'unhide' },
{ type: 'separator' },
{ label: '退出', accelerator: 'Command+Q', role: 'quit' }
]
});
}
// 构建并设置菜单
const menu = Menu.buildFromTemplate(template);
Menu.setApplicationMenu(menu);
右键菜单(上下文菜单) 可以通过 IPC 在渲染进程中触发,并在主进程中创建显示。下面的例子实现了一个包含复制、粘贴和打开链接功能的右键菜单:
// 主进程
const { Menu, MenuItem } = require('electron');
ipcMain.on('show-context-menu', (event, params) => {
const template = [
{
label: '复制',
click: () => {
event.sender.send('context-menu-command', 'copy');
}
},
{
label: '粘贴',
click: () => {
event.sender.send('context-menu-command', 'paste');
}
},
{ type: 'separator' },
{
label: '在浏览器中打开',
visible: params.linkURL !== undefined,
click: () => {
require('electron').shell.openExternal(params.linkURL);
}
}
];
const menu = Menu.buildFromTemplate(template);
menu.popup(BrowserWindow.fromWebContents(event.sender));
});
// 渲染进程(通过预加载脚本)
window.addEventListener('contextmenu', (e) => {
e.preventDefault();
ipcRenderer.send('show-context-menu', {
linkURL: e.target.href || undefined
});
});
ipcRenderer.on('context-menu-command', (event, command) => {
if (command === 'copy') {
document.execCommand('copy');
} else if (command === 'paste') {
document.execCommand('paste');
}
});
动态菜单 允许根据应用状态更新菜单项。你可以更新现有菜单项的状态,或者完全重新构建菜单:
// 根据应用状态动态更新菜单
function updateMenu(isPlaying) {
const menu = Menu.getApplicationMenu();
const playbackMenu = menu.items.find(item => item.label === '播放');
if (playbackMenu) {
playbackMenu.submenu.items[0].enabled = !isPlaying; // 播放按钮
playbackMenu.submenu.items[1].enabled = isPlaying; // 暂停按钮
}
}
// 或者完全重建菜单
function rebuildMenu(userData) {
const template = [
{
label: '用户',
submenu: [
{
label: userData.name,
enabled: false
},
{ type: 'separator' },
{
label: '登出',
click: () => {
// 登出逻辑
}
}
]
},
// ... 其他菜单项
];
const menu = Menu.buildFromTemplate(template);
Menu.setApplicationMenu(menu);
}
5. dialog 模块
dialog 模块用于显示系统原生的对话框,如文件选择、保存和消息框。
文件/目录选择对话框 可以配置多种选项。下面的例子展示了如何打开文件和保存文件:
const { dialog } = require('electron');
// 打开文件对话框
ipcMain.handle('dialog:openFile', async () => {
const result = await dialog.showOpenDialog({
title: '选择文件',
defaultPath: app.getPath('documents'),
buttonLabel: '打开',
filters: [
{ name: '图片', extensions: ['jpg', 'png', 'gif'] },
{ name: '文档', extensions: ['pdf', 'doc', 'docx'] },
{ name: '所有文件', extensions: ['*'] }
],
properties: [
'openFile',
'openDirectory',
'multiSelections',
'showHiddenFiles',
'createDirectory',
'promptToCreate'
]
});
if (result.canceled) {
return { canceled: true };
}
return {
canceled: false,
filePaths: result.filePaths
};
});
// 保存文件对话框
ipcMain.handle('dialog:saveFile', async (event, content) => {
const result = await dialog.showSaveDialog({
title: '保存文件',
defaultPath: path.join(app.getPath('documents'), 'untitled.txt'),
buttonLabel: '保存',
filters: [
{ name: '文本文件', extensions: ['txt'] },
{ name: '所有文件', extensions: ['*'] }
],
properties: [
'createDirectory',
'showOverwriteConfirmation'
]
});
if (result.canceled) {
return { canceled: true };
}
// 写入文件
try {
await fs.promises.writeFile(result.filePath, content, 'utf-8');
return { success: true, filePath: result.filePath };
} catch (error) {
return { success: false, error: error.message };
}
});
消息框 用于向用户显示信息或获取确认。推荐使用异步版本以避免阻塞主进程:
// 异步消息框(推荐)
ipcMain.handle('dialog:showMessage', async (event, options) => {
const result = await dialog.showMessageBox({
type: 'info',
title: '提示',
message: '主要消息',
detail: '详细说明文本',
buttons: ['确定', '取消', '更多'],
defaultId: 0,
cancelId: 1,
noLink: false,
checkboxLabel: '不再提示',
checkboxChecked: false
});
return {
response: result.response,
checkboxChecked: result.checkboxChecked
};
});
// 同步消息框(阻塞主进程,慎用)
const choice = dialog.showMessageBoxSync(mainWindow, {
type: 'question',
buttons: ['是', '否'],
title: '确认',
message: '确定要删除吗?'
});
if (choice === 0) {
// 用户点击"是"
}
错误对话框 可以快速显示错误信息:
// 显示错误框(简单版本)
dialog.showErrorBox('错误', '发生了一个错误,请重试。');
// 或使用消息框显示更详细的错误
dialog.showMessageBox({
type: 'error',
title: '操作失败',
message: '无法保存文件',
detail: error.stack,
buttons: ['确定']
});
6. shell 模块
shell 模块提供与桌面集成的功能,如打开外部链接、在文件管理器中显示文件等。
const { shell } = require('electron');
// 在默认浏览器中打开 URL
ipcMain.handle('shell:openExternal', async (event, url) => {
try {
await shell.openExternal(url);
return { success: true };
} catch (error) {
return { success: false, error: error.message };
}
});
// 在文件管理器中显示文件
ipcMain.handle('shell:showItemInFolder', async (event, fullPath) => {
shell.showItemInFolder(fullPath);
});
// 用默认应用打开文件
ipcMain.handle('shell:openPath', async (event, path) => {
const error = await shell.openPath(path);
if (error) {
return { success: false, error };
}
return { success: true };
});
// 将文件移到回收站
ipcMain.handle('shell:trashItem', async (event, path) => {
try {
await shell.trashItem(path);
return { success: true };
} catch (error) {
return { success: false, error: error.message };
}
});
// 播放提示音
shell.beep();
// 写入剪贴板书签(macOS)
shell.writeShortcutLink(shortcutPath, operation, options);
为了安全地在渲染进程中使用这些功能,需要通过预加载脚本暴露 API:
// preload.js
contextBridge.exposeInMainWorld('shell', {
openExternal: (url) => ipcRenderer.invoke('shell:openExternal', url),
showItemInFolder: (path) => ipcRenderer.invoke('shell:showItemInFolder', path),
openPath: (path) => ipcRenderer.invoke('shell:openPath', path),
trashItem: (path) => ipcRenderer.invoke('shell:trashItem', path)
});
// renderer.js
document.getElementById('openBrowser').addEventListener('click', async () => {
await window.shell.openExternal('https://electronjs.org');
});
document.getElementById('showFile').addEventListener('click', async () => {
await window.shell.showItemInFolder('/path/to/file.txt');
});
7. webContents 模块
webContents 负责渲染和控制网页,是 BrowserWindow 的核心属性,提供了丰富的页面控制功能。
const { BrowserWindow } = require('electron');
const win = new BrowserWindow({ width: 800, height: 600 });
const contents = win.webContents;
// 加载 URL
contents.loadURL('https://example.com');
// 执行 JavaScript
contents.executeJavaScript(`
document.body.style.backgroundColor = 'red';
return document.title;
`).then(title => {
console.log('页面标题:', title);
});
// 插入 CSS
contents.insertCSS('body { background-color: blue; }');
// 打印页面
contents.print({
silent: false,
printBackground: true,
deviceName: '',
color: true,
margins: {
marginType: 'printableArea'
},
landscape: false,
scaleFactor: 100,
pagesPerSheet: 1,
collate: false,
copies: 1
});
// 打印为 PDF
contents.printToPDF({
marginsType: 0,
pageSize: 'A4',
printBackground: true,
printSelectionOnly: false,
landscape: false
}).then(data => {
fs.writeFile('/path/to/output.pdf', data, (error) => {
if (error) throw error;
console.log('PDF 生成成功');
});
});
// 截图
contents.capturePage().then(image => {
fs.writeFile('/path/to/screenshot.png', image.toPNG(), (error) => {
if (error) throw error;
console.log('截图保存成功');
});
});
// 缩放
contents.setZoomFactor(1.5);
contents.setZoomLevel(1);
// 查找文本
contents.findInPage('search term', {
forward: true,
matchCase: false,
});
// 停止查找
contents.stopFindInPage('clearSelection');
// 开始/停止加载
contents.reload();
contents.reloadIgnoringCache();
contents.stop();
// 历史导航
contents.goBack();
contents.goForward();
contents.goToIndex(1);
contents.goToOffset(-1);
// 判断状态
const canGoBack = contents.canGoBack();
const canGoForward = contents.canGoForward();
const isLoading = contents.isLoading();
const isWaitingForResponse = contents.isWaitingForResponse();
// 发送消息到渲染进程
contents.send('channel-name', arg1, arg2);
// 事件监听
contents.on('did-finish-load', () => {
console.log('页面加载完成');
});
contents.on('did-fail-load', (event, errorCode, errorDescription) => {
console.error('页面加载失败:', errorDescription);
});
contents.on('did-navigate', (event, url) => {
console.log('导航到:', url);
});
contents.on('new-window', (event, url) => {
event.preventDefault();
// 自定义处理
});
// 控制台消息
contents.on('console-message', (event, level, message, line, sourceId) => {
console.log(`[${level}] ${message} (${sourceId}:${line})`);
});
8. nativeImage 模块
nativeImage 用于创建和操作图片,支持从多种来源创建图片,并进行调整大小、裁剪等操作。
const { nativeImage } = require('electron');
// 从文件创建图片
const image = nativeImage.createFromPath('/path/to/icon.png');
// 从 Buffer 创建
const buffer = fs.readFileSync('/path/to/icon.png');
const image = nativeImage.createFromBuffer(buffer);
// 从 Data URL 创建
const dataURL = 'data:image/png;base64,iVBORw0KG...';
const image = nativeImage.createFromDataURL(dataURL);
// 创建空图片
const image = nativeImage.createEmpty();
// 获取图片信息
const size = image.getSize();
const aspectRatio = image.getAspectRatio();
const isEmpty = image.isEmpty();
// 转换格式
const pngBuffer = image.toPNG();
const jpegBuffer = image.toJPEG(80);
const dataURL = image.toDataURL();
const bitmap = image.toBitmap();
// 调整大小
const resized = image.resize({
width: 64,
height: 64,
quality: 'best'
});
// 裁剪
const cropped = image.crop({
x: 0,
y: 0,
width: 100,
height: 100
});
// 设置模板图片(macOS 菜单栏图标)
image.setTemplateImage(true);
// 应用场景示例
const tray = new Tray(nativeImage.createFromPath('icon.png'));
const icon = nativeImage.createFromPath('icon.png').resize({ width: 16, height: 16 });
9. session 模块
session 管理浏览器会话、cookie、缓存、代理和请求拦截等,是实现网络功能控制的关键。
const { session } = require('electron');
// 默认会话
const ses = session.defaultSession;
// 或创建自定义会话
const customSession = session.fromPartition('persist:mypartition');
// Cookie 管理
ipcMain.handle('session:getCookies', async (event, url) => {
const cookies = await ses.cookies.get({ url });
return cookies;
});
ipcMain.handle('session:setCookie', async (event, cookie) => {
await ses.cookies.set(cookie);
});
ipcMain.handle('session:removeCookie', async (event, url, name) => {
await ses.cookies.remove(url, name);
});
// 清除缓存
ipcMain.handle('session:clearCache', async () => {
await ses.clearCache();
});
// 清除存储数据
ipcMain.handle('session:clearStorageData', async (options) => {
await ses.clearStorageData({
storages: ['appcache', 'cookies', 'filesystem', 'indexdb', 'localstorage', 'shadercache', 'websql', 'serviceworkers', 'cachestorage']
});
});
// 设置下载路径
ses.on('will-download', (event, item, webContents) => {
// 设置保存路径
item.setSavePath(path.join(app.getPath('downloads'), item.getFilename()));
// 下载进度
item.on('updated', (event, state) => {
if (state === 'interrupted') {
console.log('下载中断');
} else if (state === 'progressing') {
if (item.isPaused()) {
console.log('下载暂停');
} else {
console.log(`已下载 ${item.getReceivedBytes()} / ${item.getTotalBytes()} 字节`);
webContents.send('download-progress', {
progress: (item.getReceivedBytes() / item.getTotalBytes()) * 100
});
}
}
});
// 下载完成
item.once('done', (event, state) => {
if (state === 'completed') {
console.log('下载成功:', item.getSavePath());
webContents.send('download-complete', {
filePath: item.getSavePath()
});
} else {
console.log(`下载失败: ${state}`);
}
});
});
// 设置代理
ses.setProxy({
proxyRules: 'http://proxy.example.com:8080',
proxyBypassRules: 'localhost,127.0.0.1'
});
// 拦截请求
ses.webRequest.onBeforeRequest((details, callback) => {
if (details.url.includes('tracking')) {
callback({ cancel: true });
} else {
callback({ cancel: false });
}
});
// 修改请求头
ses.webRequest.onBeforeSendHeaders((details, callback) => {
details.requestHeaders['User-Agent'] = 'MyElectronApp/1.0';
callback({ requestHeaders: details.requestHeaders });
});
// 修改响应头
ses.webRequest.onHeadersReceived((details, callback) => {
callback({
responseHeaders: {
...details.responseHeaders,
'Content-Security-Policy': ['default-src \'self\'']
}
});
});
10. clipboard 模块
clipboard 模块提供系统剪贴板的读写功能,支持文本、HTML、图片和 RTF 等多种格式。
const { clipboard } = require('electron');
// 读取文本
const text = clipboard.readText();
console.log('剪贴板文本:', text);
// 写入文本
clipboard.writeText('Hello Electron');
// 读取 HTML
const html = clipboard.readHTML();
// 写入 HTML
clipboard.writeHTML('<b>Hello</b> <i>Electron</i>');
// 读取图片
const image = clipboard.readImage();
if (!image.isEmpty()) {
fs.writeFileSync('/path/to/clipboard.png', image.toPNG());
}
// 写入图片
const image = nativeImage.createFromPath('/path/to/image.png');
clipboard.writeImage(image);
// 读取 RTF
const rtf = clipboard.readRTF();
// 写入 RTF
clipboard.writeRTF('{\\rtf1\\ansi\\b Hello\\b0}');
// 读取书签(macOS)
const bookmark = clipboard.readBookmark();
// 写入书签
clipboard.writeBookmark('标题', 'https://electronjs.org');
// 清空剪贴板
clipboard.clear();
// 多格式写入
clipboard.write({
text: 'Plain text',
html: '<b>Bold text</b>',
rtf: '{\\rtf1\\ansi\\b RTF text\\b0}',
image: nativeImage.createFromPath('/path/to/image.png')
});
// 获取支持的格式
const formats = clipboard.availableFormats();
console.log('剪贴板格式:', formats);
// IPC 示例
ipcMain.handle('clipboard:readText', () => {
return clipboard.readText();
});
ipcMain.handle('clipboard:writeText', (event, text) => {
clipboard.writeText(text);
return { success: true };
});
ipcMain.handle('clipboard:readImage', () => {
const image = clipboard.readImage();
if (image.isEmpty()) {
return null;
}
return image.toDataURL();
});
// preload.js
contextBridge.exposeInMainWorld('clipboard', {
readText: () => ipcRenderer.invoke('clipboard:readText'),
writeText: (text) => ipcRenderer.invoke('clipboard:writeText', text),
readImage: () => ipcRenderer.invoke('clipboard:readImage')
});
// renderer.js
document.getElementById('copyBtn').addEventListener('click', async () => {
const text = document.getElementById('textInput').value;
await window.clipboard.writeText(text);
console.log('已复制到剪贴板');
});
document.getElementById('pasteBtn').addEventListener('click', async () => {
const text = await window.clipboard.readText();
document.getElementById('textInput').value = text;
});
应用构建与发布
应用构建与发布是 Electron 开发的最后阶段,涉及打包、签名、制作安装程序和发布到各平台。Electron Forge 提供了完整的工具链支持。
构建与发布概述
Electron Forge 构建流程主要分为三步:
- Package(打包):将源代码和 Electron 运行时打包成可执行应用。
- Make(制作):基于打包结果创建平台特定的安装程序。
- Publish(发布):将安装程序上传到发布平台。
配置文件通常是 forge.config.js 或 forge.config.ts:
module.exports = {
packagerConfig: {
asar: true,
icon: './assets/icon',
appBundleId: 'com.company.app',
appCategoryType: 'public.app-category.productivity',
win32metadata: {
CompanyName: 'My Company',
FileDescription: 'My Electron App',
ProductName: 'My App'
},
osxSign: {
identity: 'Developer ID Application: Company Name (TEAM_ID)',
hardenedRuntime: true,
entitlements: 'entitlements.plist',
'entitlements-inherit': 'entitlements.plist',
'signature-flags': 'library'
},
osxNotarize: {
appleId: process.env.APPLE_ID,
appleIdPassword: process.env.APPLE_ID_PASSWORD,
teamId: process.env.APPLE_TEAM_ID
}
},
rebuildConfig: {},
makers: [
// 不同平台的安装包制作器
],
publishers: [
// 发布配置
],
plugins: [
// 插件配置
]
};
执行命令:
# 打包应用(不创建安装程序)
npm run package
# 制作安装包
npm run make
# 发布应用
npm run publish
# 指定平台和架构
npm run make -- --platform=darwin --arch=x64
npm run make -- --platform=win32 --arch=ia32,x64
npm run make -- --platform=linux --arch=x64
图标配置
不同平台对图标格式和尺寸有不同的要求:
- Windows:需要
.ico格式,包含多个尺寸(如 16x16, 32x32, 48x48, 256x256)。 - macOS:需要
.icns格式,包含从 16x16 到 1024x1024 的多个尺寸(含 Retina @2x 版本)。 - Linux:通常使用
.png格式,推荐 512x512 或 1024x1024。
在 Forge 配置中,只需指定不含扩展名的图标路径,它会自动选择对应平台的文件:
packagerConfig: {
icon: './assets/icons/icon', // 自动选择 icon.ico、icon.icns 或 icon.png
}
不同 Maker 的作用
Maker 用于创建平台特定的安装程序。常见的 Maker 包括:
- Squirrel.Windows:为 Windows 创建 Setup.exe 安装程序,支持自动更新。
- WiX MSI:使用 WiX 工具集创建企业级的 MSI 安装包。
- DMG:为 macOS 创建磁盘镜像安装包,提供拖拽安装体验。
- deb/RPM:分别为 Debian/Ubuntu 和 Red Hat/Fedora Linux 创建安装包。
- ZIP:创建简单的绿色版压缩包,适合所有平台便携分发。
完整 Maker 配置示例:
makers: [
// Windows
{
name: '@electron-forge/maker-squirrel',
platforms: ['win32'],
config: {
name: 'myapp',
authors: 'My Company',
exe: 'myapp.exe'
}
},
// macOS
{
name: '@electron-forge/maker-dmg',
platforms: ['darwin'],
config: {
format: 'ULFO'
}
},
// Linux
{
name: '@electron-forge/maker-deb',
platforms: ['linux'],
config: {}
},
{
name: '@electron-forge/maker-rpm',
platforms: ['linux'],
config: {}
},
// 所有平台 - ZIP 备份
{
name: '@electron-forge/maker-zip',
platforms: ['darwin', 'linux', 'win32']
}
]
Publisher
Publisher 负责将构建好的安装包发布到各种平台和服务。
GitHub Publisher 是最常用的方式,它可以自动创建 GitHub Release 并上传文件:
publishers: [
{
name: '@electron-forge/publisher-github',
config: {
repository: {
owner: 'your-username',
name: 'your-repo'
},
prerelease: false,
draft: true,
authToken: process.env.GITHUB_TOKEN
}
}
]
需要在环境中设置 GITHUB_TOKEN(具有 repo 权限)。可以通过在推送标签时触发 GitHub Actions 工作流来实现自动发布。
其他 Publisher 包括:
- S3:上传到 AWS S3 存储桶。
- Electron Release Server:发布到自托管的更新服务器。
- Snapcraft:发布到 Ubuntu Snap Store。
发布最佳实践:
- 遵循语义化版本控制。
- 为 macOS 和 Windows 应用进行代码签名和公证。
- 配置
electron-updater以实现应用内自动更新。 - 采用分阶段发布(如先 Beta 后 Stable)。
- 编写清晰的 Release Notes。
- 考虑使用多个 Publisher 进行备份。