快速入门 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 模块用于网络请求日志记录。
相关推荐
再吃一根胡萝卜42 分钟前
08 · 前端:Vue3 + SSE 流式与执行链路可视化
面试
再吃一根胡萝卜1 小时前
03 · 后端:FastAPI 与分层架构
面试
mldong2 小时前
你的 Vue3 项目也能有钉钉同款审批流设计器:npm 装包,10 分钟画出第一条审批流
前端·vue.js
2分钟速写快排3 小时前
什么是 RAG?如何用 RAG 实现一个用户记忆?
前端·后端·ai编程
passerby60614 小时前
如何自己造一个时间处理库
前端·javascript·github
走到天涯海角5 小时前
react里面的长列表渲染优化
前端·react.js·前端框架
小羊没烦恼!5 小时前
Hello Web API系列教程——Web API与国际化
java·服务器·前端·javascript·php
北岛贰5 小时前
迷茫焦虑期,我做了一个带支付带官网的 AI 聊天虚拟恋人 App
前端·人工智能·后端
Interview Aid1125 小时前
Walmart Global Tech SDE 三轮面经|基础、并发、压力面
面试·职场和发展
mayaairi6 小时前
Vue2 组件通讯(三):全局事件总线、PubSub、插槽与组件实例属性
前端·javascript·vue.js