桌面开发与 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: falsecontextIsolation: truesandbox: true
结尾金句:"渲染进程的代码用户能改,假设它已经被攻破——最坏情况也只能调我白名单暴露的方法。"
6. 高频追问
- 多窗口状态同步:主进程中转,
BrowserWindow.getAllWindows().forEach(win => win.webContents.send(...)) - 渲染进程之间能直接通信吗:同源可以。
window.postMessage/MessageChannel/BroadcastChannel都是同源可用的直连通道;Electron 还提供webContents.postMessage+MessageChannelMain跨窗口直传。只有跨域才必须经主进程中转 - 为什么不用
sendSync:阻塞渲染进程,主线程卡死
面试延伸:Electron vs Tauri
近年来,基于 Rust 的 Tauri 成为桌面开发的新宠,面试官经常会让你对比两者:
| 特性 | Electron | Tauri |
|---|---|---|
| 底层语言 | C++ / Node.js | Rust |
| 渲染引擎 | 打包内置了完整的 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