快速入门 Electron

1. Electron的运行原理是什么?

Electron的核心是Chromium + Node.js的组合。它将Chromium(负责渲染界面)和Node.js(提供操作系统底层访问能力)合并到同一个进程中。运行时分为主进程和渲染进程:主进程是入口,控制应用生命周期;渲染进程则展示UI。两者通过进程间通信(IPC)协同工作

2. 主进程和渲染进程的区别?各自能做什么?

对比项 主进程 (Main Process) 渲染进程 (Renderer Process)
数量 唯一(app 生命周期内一个实例) 每个 BrowserWindow 对应一个独立渲染进程
入口 main.js(通过 package.jsonmain 指定) 每个窗口加载的 HTML 及其 JS
Node.js 环境 完全支持(require, fs, child_process 等) 默认关闭(nodeIntegration: false,安全考虑)
窗口管理 创建、控制、销毁 BrowserWindow 不能创建新窗口,但可通过 IPC 请求主进程创建
系统原生 GUI 可调用 Menu, Tray, Notification, dialog 等模块 不可直接调用,需通过 IPC 委托给主进程
生命周期 应用启动时创建,退出时销毁 窗口关闭时销毁,但可隐藏而非关闭
调试方式 命令行启动时使用 --inspect--inspect-brk Chrome DevTools(Ctrl+Shift+I
进程间通信 通过 ipcMain 监听 / 回复 通过 ipcRenderer 发送 / 监听

3. 通讯方式(进程间通信 IPC)

Electron 基于 Chromium 的多进程架构,主进程渲染进程默认相互隔离,必须通过 IPC 通信。

3.1 核心模块

  • 主进程ipcMain
  • 渲染进程ipcRenderer
  • 安全暴露 APIcontextBridge(配合预加载脚本)

3.2 常用通信模式

模式 方向 API 特点
异步发送(单向) 渲染 → 主 ipcRenderer.send + ipcMain.on 不等待回应
异步请求-回复(双向) 渲染 → 主 → 渲染 ipcRenderer.invoke + ipcMain.handle 推荐,返回 Promise
同步发送(阻塞) 渲染 → 主 ipcRenderer.sendSync 阻塞渲染进程,不推荐
主进程主动发消息 主 → 特定渲染 win.webContents.send(channel, data) 需要获取窗口对象
主进程广播 主 → 所有渲染 遍历 BrowserWindow.getAllWindows() 依次发送 自定义实现

2.3 ipc 通信示例

步骤1:编写预加载脚本

javascript 复制代码
// preload.js
const { contextBridge, ipcRenderer } = require('electron');

// 暴露安全的 API 给渲染进程
contextBridge.exposeInMainWorld('electronAPI', {
  // 获取应用版本(异步调用主进程)
  getAppVersion: () => ipcRenderer.invoke('get-app-version'),

  // 保存文件(暴露文件保存能力)
  saveFile: (content) => ipcRenderer.invoke('save-file', content),

  // 监听来自主进程的消息(如系统主题变化)
  onThemeChange: (callback) => {
    ipcRenderer.on('theme-changed', (event, newTheme) => callback(newTheme));
  },

  // 移除监听,避免内存泄漏
  removeThemeListener: () => {
    ipcRenderer.removeAllListeners('theme-changed');
  }
});

// 可选:在 preload 中做一些初始化,但不要操作 DOM(因为页面还未加载)
console.log('Preload script loaded');

步骤2:在主进程中引用预加载脚本

javascript 复制代码
// main.js
const { BrowserWindow } = require('electron');
const path = require('path');

const win = new BrowserWindow({
  width: 800,
  height: 600,
  webPreferences: {
    preload: path.join(__dirname, 'preload.js'),
    // 务必保持 false(安全)
    nodeIntegration: false,
    contextIsolation: true
  }
});

// 主进程处理消息
ipcMain.handle('get-app-version', () => {
  return app.getVersion();
});

ipcMain.handle('save-file', async (event, content) => {
  // 使用 fs 写入文件
  const fs = require('fs').promises;
  const path = require('path');
  const filePath = path.join(app.getPath('userData'), 'myfile.txt');
  await fs.writeFile(filePath, content);
  return filePath;
});

// 主进程主动发消息(例如改变主题)
win.webContents.on('did-finish-load', () => {
  win.webContents.send('theme-changed', 'dark');
});

步骤3:在渲染进程中使用暴露的 API

javascript 复制代码
// renderer.js (运行在网页中)
(async () => {
  // 调用暴露的方法
  const version = await window.electronAPI.getAppVersion();
  console.log('App version:', version);

  document.getElementById('saveBtn').addEventListener('click', async () => {
    const path = await window.electronAPI.saveFile('Hello Electron');
    console.log('File saved at:', path);
  });

  // 监听主进程消息
  window.electronAPI.onThemeChange((newTheme) => {
    document.body.className = `theme-${newTheme}`;
  });
})();

3.4 渲染进程之间通信

  • 通过主进程转发:一个渲染进程发消息给主进程,主进程再转发给另一个渲染进程。
  • Web 原生方法MessageChannel / BroadcastChannel(同源窗口可用,但注意 Electron 多窗口可能跨 webContents,推荐前者)。

4. 预加载脚本和普通渲染进程脚本有什么区别?

对比项 预加载脚本 普通渲染进程脚本
执行时间 HTML 加载前 HTML 解析过程中或之后
可访问 Node.js 是(通过 require)只有一部分NodeAPI可使用 否(默认 nodeIntegration: false
环境隔离 运行在"隔离世界",通过 contextBridge 暴露 API给渲染进程 运行在"主要世界"(即正常网页的 window
是否可以操作 DOM 技术上可以,但不推荐 完全可以(常规前端代码)

4.1 关键安全原则

原则 说明
永不直接暴露 Node.js 模块 不要 exposeInMainWorld('fs', require('fs'))
验证所有输入参数 防止路径遍历、命令注入
保持 contextIsolation: true 隔离预加载环境与前端 window 对象
最小化暴露 API 只暴露必需的方法

4.2 常见面试点

  • 执行时机:渲染进程创建后、HTML 加载前。
  • 与普通渲染脚本区别:预加载脚本拥有 Node.js 权限,普通脚本没有。
  • 多个窗口:可以共享同一个预加载脚本,但每个窗口独立实例。
  • 操作 DOM :技术上可以,但推荐用 DOMContentLoaded 事件,或者交给普通渲染脚本去做。

五、优化方式(性能与体验)

5.1 启动速度优化

  • 懒加载窗口ready-to-show 事件后再 show() 窗口,避免白屏。
  • 使用 backgroundThrottling: false:防止隐藏页面被节流(需要时)。
  • 预加载优化:预加载脚本保持精简,不要执行耗时同步操作。
  • 模块按需引入require 时动态加载,避免启动时加载全部依赖。

5.2 运行时性能优化

  • 启用 nodeIntegration: false:避免渲染进程直接访问 Node.js,降低安全风险并减少上下文切换开销。
  • 合理使用 Web Workers:CPU 密集任务放到 Worker 线程。
  • 避免内存泄漏 :及时移除事件监听、销毁不用的窗口 (destroy())、使用 WeakMap 管理缓存。
  • 限制渲染帧率 :使用 requestAnimationFrame 控制不必要的 UI 刷新。
  • 使用 requestIdleCallback 或任务分片
  • 使用 will-navigate 等事件拦截不必要的导航
  • 减少不必要的 IPC 调用

5.3 渲染优化(同常规 Web 优化)

  • 使用 Chrome DevTools 分析重排/重绘。
  • 大列表使用虚拟滚动(如 react-window)。
  • 图片懒加载、压缩。
  • 防止 JavaScript 长时间阻塞主线程。

六、打包体积大小控制

Electron 应用体积大的原因:内置了完整 Chromium + Node.js (约 60--80MB 基础体积)。额外体积来自于 node_modules 和源码。

6.1 常用瘦身手段

方法 效果 备注
使用 electron-builderasar 打包 减少文件数量,不影响体积 必须开启
排除无用模块(devDependencies 可减少几十 MB 在打包配置中 node_modules 过滤
删除不必要的二进制文件(如 .node 原生模块中多余的平台文件) 数 MB 手动清理或使用 electron-builderafterPack 钩子
使用 webpack / vite 打包前端代码 减少冗余代码,合并文件 显著减小 app.asar 体积
不打包源映射文件(.map 减小 20-40% 生产环境关闭 devtool
升级 Electron 版本 新版本有时会优化基础库大小 评估兼容性
使用 electron-builderfile 配置精细控制哪些文件进入 精准瘦身 可排除测试、文档、示例等

6.2 典型打包配置 (electron-builder)

json

json 复制代码
{
  "directories": {
    "output": "dist"
  },
  "files": [
    "main.js",
    "preload.js",
    "renderer/**/*",
    "!**/*.map",
    "!**/test/**"
  ],
  "asar": true,
  "nodeModules": ["prod"]   // 仅打包生产依赖
}

6.3 体积对比参考

  • 空白 Electron 应用打包后大约 70--90 MB(Windows exe)。
  • 优化后(剔除多余依赖、压缩前端代码)约 50--70 MB
  • 极致优化(使用 electron-builder + 7z 压缩 + 增量更新)最小可达 40 MB 左右

七、常用系统 API 模块详解

7.1. 通知(Notifications)

用于从应用向操作系统发送原生桌面通知。

  • 使用位置:主进程和渲染进程均可使用,但实现方式不同。

  • API 模块 :主进程使用 Notification 模块;渲染进程则直接使用 HTML5 的 Notification API。

  • 代码示例

    dart 复制代码
    // 主进程
    new Notification({ 
        title: '标题', 
        body: '这是通知的内容', 
        icon: './icon.png' 
    }).show();
    javascript 复制代码
    // 渲染进程
    new window.Notification('标题', { body: '这是通知的内容' });

7.2. 系统托盘(Tray)

允许应用在操作系统的通知区域(Windows 任务栏右侧或 macOS 菜单栏右侧)显示一个图标,并提供右键菜单和交互功能。

  • 使用位置:仅限于主进程。

  • API 模块Tray 模块。

  • 注意事项 :在 macOS 上,建议使用"模板图片"(Template Image)作为图标,这样图标可以自动适应浅色和深色模式的菜单栏。托盘图标的创建需要在应用 ready 事件之后。

  • 代码示例

    ini 复制代码
    const { app, Tray, Menu } = require('electron');
    let tray = null;
    app.whenReady().then(() => {
        tray = new Tray('./path/to/icon.png');
        const contextMenu = Menu.buildFromTemplate([
            { label: '显示应用', click: () => { win.show(); } },
            { label: '退出', click: () => { app.quit(); } }
        ]);
        tray.setContextMenu(contextMenu);
        tray.setToolTip('我的应用');
    });

7.3. 对话框(Dialogs)

用于调用系统原生的对话框,例如:打开文件、保存文件、消息提示等。

  • 使用位置:主进程。

  • API 模块dialog 模块。

  • 常见 API

    • dialog.showOpenDialogSync(): 同步打开文件选择对话框。
    • dialog.showSaveDialog(): 打开保存文件对话框。
    • dialog.showMessageBox(): 打开一个消息提示框。
  • 注意事项 :因为对话框会阻塞窗口,建议使用异步方法如 showOpenDialog,以免 UI 卡死。showMessageBox 可用于确认等操作,返回值会包含用户点击的按钮索引。

7.4. 剪贴板(Clipboard)

用于读写系统剪贴板上的文本、图片、文件等数据。

  • 使用位置:主进程和渲染进程均可使用。

  • API 模块clipboard 模块。

  • 代码示例

    arduino 复制代码
    const { clipboard } = require('electron');
    clipboard.writeText('Hello, Electron!');
    const text = clipboard.readText();
    console.log(text); // 输出: Hello, Electron!

7.5. 全局快捷键(Global Shortcuts)

允许应用注册系统级别的快捷键,即使应用在后台或没有焦点,也能响应用户的按键组合。

  • 使用位置:主进程。

  • API 模块globalShortcut 模块。

  • 注意事项 :必须在应用 ready 事件之后才能注册。为避免冲突,建议在应用退出时注销所有已注册的快捷键。

  • 代码示例

    javascript 复制代码
    const { app, globalShortcut } = require('electron');
    app.whenReady().then(() => {
        globalShortcut.register('CommandOrControl+X', () => {
            console.log('CommandOrControl+X is pressed');
        });
    });
    app.on('will-quit', () => { globalShortcut.unregisterAll(); });

7.6. 电源管理(Power Management)

用于监控系统电源状态,或阻止系统进入睡眠模式(例如在下载或播放视频时)。

  • 使用位置:主进程。

  • API 模块powerMonitorpowerSaveBlocker 模块。

  • 常见 API

    • powerMonitor.getSystemIdleState(): 获取系统空闲状态。
    • powerSaveBlocker.start(): 开始阻止系统睡眠。
    • powerSaveBlocker.stop(id): 停止阻止系统睡眠。
  • powerSaveBlocker 使用场景

    • prevent-app-suspension: 阻止应用被挂起(例如后台下载时)。
    • prevent-display-sleep: 阻止显示器关闭(例如全屏播放视频时),后者的优先级高于前者。

7.7. 菜单(Menu)

用于创建自定义的应用程序菜单(位于窗口标题栏下方),取代系统默认的 Electron 菜单。

  • 使用位置:主进程。

  • API 模块Menu 模块。

  • 常见 API

    • Menu.setApplicationMenu(): 设置应用程序的菜单栏。
    • Menu.buildFromTemplate(): 通过模板构建菜单。

7.8. 原生主题(Native Theme)

用于获取和监听操作系统的原生主题(浅色/深色模式)变化。

  • 使用位置:主进程和渲染进程。

  • API 模块nativeTheme 模块。

  • 常见 API

    • nativeTheme.shouldUseDarkColors: 检查当前是否使用深色主题。
    • nativeTheme.on('updated'): 监听主题变化事件。

7.9. 其他系统交互

  • 屏幕信息 :使用 screen 模块可以获取系统显示器的信息,如尺寸、分辨率、DPI 缩放等。
  • 打印 :通过 webContents.print()webContents.printToPDF() 实现网页打印或将网页保存为 PDF。
  • 网络状态 :使用 net 模块可以发起原生 HTTP/HTTPS 请求,netLog 模块用于网络请求日志记录。
相关推荐
还有多久拿退休金9 小时前
LLM应用开发一:给失忆的大模型装上"脑子"——LangChain.js对话记忆从零实战
前端·llm
思考着亮9 小时前
1.window.location.href 和 router.push 跳转方式
前端
ZengLiangYi9 小时前
插件式架构设计:SourceAdapter 接口抽象
前端·javascript·后端
万岳科技系统开发9 小时前
私域直播系统开发从0到1:企业直播平台搭建全过程
前端·小程序·架构
出海小龙9 小时前
联盟营销实战技能体系:从市场研究到数据优化的完整盈利框架
大数据·前端·人工智能
code_Bo9 小时前
apple gpt 礼品卡订阅失败解决方案
前端·人工智能·后端
转转技术团队10 小时前
MCP 解析:给 AI 装上“万能充电口”,打通连接世界的“最后一公里”
前端
Y敲键盘的地方10 小时前
第9章 工具调用循环——Agent的行动闭环
java·服务器·前端
苏瞳儿10 小时前
vue3+pinia+mqtt实时响应连接
前端·javascript·vue.js