Skip to main content

桌面开发与 Electron

Electron 是一个使用 Web 技术栈(HTML、CSS 和 JavaScript)构建跨平台桌面应用程序的框架。

面试考点:Electron 的三大底层核心

  • Chromium:提供强大的 Web 渲染引擎。开发者可以直接使用最新的 CSS 布局(如 Flex/Grid)和 ES6+ 语法,完全不需要考虑浏览器兼容性
  • Node.js:提供本地系统的底层访问能力(如文件读写 fs、网络请求、本地命令调用等)。
  • Native APIs:Electron 封装了统一的原生 API,用于调用操作系统的原生能力(窗口、托盘、菜单、通知、全局快捷键等)。只能在主进程调用(preload 也用不了)。

常用代码示例:

// 1. 窗口
new BrowserWindow({
width: 1024,
height: 768,
webPreferences: {
/* ... */
},
});

// 2. 系统托盘
new Tray("icon.png").setContextMenu(
Menu.buildFromTemplate([
{ label: "打开", click: () => mainWindow.show() },
{ label: "退出", click: () => app.quit() },
]),
);

// 3. 系统通知
new Notification({ title: "消息", body: "你有一条新消息" }).show();

// 4. 全局快捷键
globalShortcut.register("CommandOrControl+Shift+I", () => {
mainWindow.webContents.openDevTools();
});

进程模型

Electron 采用多进程架构(继承自 Chromium)。面试时常被问到"主进程和渲染进程的区别"。

1. 主进程 (Main Process)

  • 唯一性:每个 Electron 应用有且只有一个主进程(通常是 main.js)。
  • 职责
    • 充当应用的"大管家",负责管理整个应用的生命周期(如启动、退出)。
    • 管理原生 GUI 元素(如创建 BrowserWindow、系统托盘、菜单)。
    • 直接运行在 Node.js 环境中,拥有最高权限,可以直接操作底层操作系统 API。

2. 渲染进程 (Renderer Process)

  • 多实例:每次调用 new BrowserWindow() 创建一个新窗口,都会产生一个新的渲染进程。
  • 职责:负责页面的 Web 渲染(执行 HTML/CSS/JS)。
  • 权限限制:出于安全考虑,现代 Electron 默认禁止在渲染进程中直接使用 Node.js API(nodeIntegration: false)。

3. 预加载脚本 (Preload Script)

  • 桥梁作用:它在渲染进程加载 HTML 之前执行。
  • 特权:它虽然运行在渲染进程中,但可以访问有限的 Node.js API(如 ipcRenderer)。
  • 最佳实践:通过 contextBridge,将安全的 API 暴露给全局 window 对象,供纯前端代码调用,这是目前官方推荐的安全通信模式。

面试延伸:Electron 通信架构设计

面试原题:"Electron 怎么做通信架构设计?"

1. 三种 IPC 模式

模式API 组合用途
invoke/handle推荐Promise 双向主进程返回数据
send/on单向通知触发副作用
sendSync同步阻塞❌ 禁

一句话:"我只用 invoke/handle,所有调用都是 Promise,方便 async/await"。

2. 三层架构

Renderer (window.api)  →  Preload (白名单)  →  Main (ipcMain.handle)
  • 渲染进程只调 window.api.xxx不接触 IPC
  • Preload 是唯一接触 ipcRenderer 的"安全岛",通过 contextBridge 暴露方法
  • 主进程统一注册 ipcMain.handle 路由,业务抽到 service 层

contextBridge 原理contextIsolation: true 开启后,渲染进程和 preload 在两个独立的 V8 上下文里——字面意义上是两个 window 对象。exposeInMainWorld 在主世界(renderer)建一个 Proxy,转发调用到独立世界(preload)。参数 / 返回值走 V8 内部序列化(只支持 JSON 可序列化的值),所以你没法传 function / Symbol 跨过去。

为啥 preload 不能用完整 Node.js:这是安全设计而非限制。配置组合决定 preload 的能力边界:

配置preload 能用
contextIsolation: true(默认)白名单:electron 部分 API(ipcRenderer / contextBridge / webFrame)+ 受限 Node.js 子集
contextIsolation: false(旧模式)完整 Node.js(不推荐
sandbox: true极严:连 require 都被 polyfill 桩替换

设计意图:渲染进程的代码用户能改,假设它已经被攻破。preload 作为"安全岛"只暴露白名单方法,最坏情况下攻击者也调不到 fs / child_process 这类危险 API。

3. 双向通信示例

// preload.ts
contextBridge.exposeInMainWorld("api", {
// 渲染 → 主:发起任务
downloadFile: (url: string) => ipcRenderer.invoke("file:download", { url }),
// 主 → 渲染:订阅进度(务必返回退订函数,避免内存泄漏)
onProgress: (cb: (p: number) => void) => {
const handler = (_: unknown, p: number) => cb(p);
ipcRenderer.on("file:progress", handler);
return () => ipcRenderer.off("file:progress", handler);
},
});

// main.ts
ipcMain.handle("file:download", async (event, { url }) => {
for (let i = 0; i <= 100; i += 10) {
await sleep(100);
event.sender.send("file:progress", i); // 主 → 渲染 推送
}
return { ok: true };
});

// 渲染进程
const stop = window.api.onProgress((p) => console.log(`进度: ${p}%`));
await window.api.downloadFile("https://example.com/file.zip");
stop(); // 组件卸载时调用,防止监听器堆积

关键点

  • event.sender.send:从 handler 里给「调用方」发消息,对应渲染进程 ipcRenderer.on 接收
  • mainWindow.webContents.send:如果要给所有窗口广播(多窗口同步状态),用这个
  • 必须退订:preload 暴露的 onXxx 一定要返回 off 闭包,否则 React 组件卸载后会泄漏监听器

5. 安全底线

  • nodeIntegration: false
  • contextIsolation: true
  • sandbox: true

结尾金句:"渲染进程的代码用户能改,假设它已经被攻破——最坏情况也只能调我白名单暴露的方法。"

6. 高频追问

  • 多窗口状态同步:主进程中转,BrowserWindow.getAllWindows().forEach(win => win.webContents.send(...))
  • 渲染进程之间能直接通信吗同源可以window.postMessage / MessageChannel / BroadcastChannel 都是同源可用的直连通道;Electron 还提供 webContents.postMessage + MessageChannelMain 跨窗口直传。只有跨域才必须经主进程中转
  • 为什么不用 sendSync:阻塞渲染进程,主线程卡死

面试延伸:Electron vs Tauri

近年来,基于 Rust 的 Tauri 成为桌面开发的新宠,面试官经常会让你对比两者:

特性ElectronTauri
底层语言C++ / Node.jsRust
渲染引擎打包内置了完整的 Chromium依赖操作系统自带的 WebView (如 macOS的WebKit、Windows的Edge WebView2)
打包体积极大(动辄 100MB+,因为自带了浏览器)极小(几MB到十几MB,因为不打包浏览器内核)
内存占用较高(多进程+V8引擎开销大)极低(Rust 原生执行 + 系统共享 WebView)
兼容性极强(各端表现完全一致)依赖系统,不同系统可能存在轻微渲染差异
适用场景复杂重型应用(VSCode、Figma、飞书)轻量级应用、对体积和内存敏感的工具

面试延伸:Electron vs 常规 Web 页面开发

面试原题:"用 Electron 开发和你平时写 Web 页面,有什么本质区别?"

Web 页面跑在浏览器的沙箱里,能力受限;Electron 跑在桌面环境里,几乎是"另一个物种"。最大区别就一句话:Web 页面"被浏览器管着",Electron"你反过来要管一堆东西"。

1. 五个核心差异

维度常规 Web 页面Electron 桌面应用
进程模型单个浏览器 Tab,一个进程跑到底多进程(main + 多个 renderer + preload + utility/GPU)
权限范围沙箱限制,只能用 Web API主进程有完整 Node.js + 原生 API(fs、child_process、托盘)
窗口控制浏览器托管(地址栏、Tab、菜单栏都是它的)完全自控:尺寸、位置、无边框、置顶、透明白定义
生命周期页面级(load/unload)应用级 + 窗口级(app.on('before-quit') / window-all-closed
分发更新部署即生效(CDN 刷新)打安装包(dmg/exe/AppImage)+ 签名 + electron-updater

2. 几个容易踩坑的"不一样"

  • 没有地址栏 / 前进后退:路由跳转完全靠 history.pushState刷新 = 404(需服务端 fallback 或 app:// 协议拦截)
  • 跨域限制大幅放宽:主进程可以直接 fetch 任何域名的资源、访问 file:// 协议(沙箱模式除外),CORS 不再是痛点
  • 离线优先:所有静态资源都跟着安装包走,天然支持离线(配合 Service Worker 还能做更精细的缓存策略)
  • 体积代价:最小空包 ~150MB(Electron + Chromium),启动比 Web 慢 2-3 秒(首次冷启动)。体积敏感就上 Tauri
  • Node API 不能直接用:即便在主进程,浏览器 API(window / document)也全部没有。需要 BrowserWindow.webContents 才能操作渲染层

3. 面试回答模板(30 秒版)

"最大的区别是运行环境:Web 页面跑在浏览器沙箱里,能力受 Web API 限制;Electron 把 Chromium 和 Node.js 拼起来,多了一套多进程架构IPC 通信,加上主进程独占的原生 API(托盘、菜单、通知)和完全自控的窗口。代价是包体积大、内存高、得多写一层 preload 通信。如果项目对体积敏感,可以考虑 Tauri。"

进阶追问

  • Q:Electron 怎么减少包体积? A:asar 压缩 + 分平台构建 + 移除不需要的 native module;想极致小直接换 Tauri
  • Q:怎么解决 Electron 首屏慢? A:BrowserWindow 加 show: false(等 ready-to-show 再显示)+ V8 snapshot + 路由懒加载
  • Q:和 PWA 区别? A:PWA 还是 Web,受浏览器沙箱限制装不上原生能力;Electron 是桌面应用,能调系统 API