# 桌面端面试题精选 Electron 与 Tauri
本章以现代
Electron与Tauri 2.x官方API为基线。重点不是背模块名,而是能说清运行时架构、权限收敛、故障恢复、性能预算与发布信任链。
# 阅读建议
- 初级岗位先完成第 1~4 组,能独立写出安全的
preload与IPC。 - 中高级岗位重点练第 5~20 组,回答时给出指标、失败路径和平台差异。
- 带交互演示的题先切换正常/边界路径,再展开详细解析复述机制。
# 1 基础架构与选型
# Electron 到底是什么?它是怎么让一个网页变成桌面应用的?
30 秒速记
Electron=Chromium+Node.js+ 一层原生桌面API,三样东西一起打进安装包,是个运行时,不是「网页套壳打包工具」- 主进程跑
Node,管应用生命周期、窗口、菜单、托盘;每个窗口的页面跑在独立的渲染进程里 - 页面要碰系统能力,走
preload+contextBridge暴露的窄接口,再用IPC交给主进程做 - 现在默认
nodeIntegration: false、contextIsolation: true、sandbox: true,页面里默认拿不到require - 能复用的是界面和业务代码;签名、安装、自动更新、菜单这些平台行为还得分平台处理
Electron 本质上是把 Chromium 和 Node.js 塞进同一个应用里,再补一层原生桌面 API,你的网页跑在它自带的浏览器内核里。 启动时先起主进程,它是个 Node 环境,负责创建窗口、处理菜单和应用生命周期;每个窗口会拉起一个渲染进程去加载页面,跟 Chrome 开一个标签页差不多。页面想读文件、弹系统对话框,不能直接调 Node,而是通过 preload 暴露的几个方法发 IPC 给主进程去做。所以它不是把网页打成 exe,而是给网页配了一整套运行时,这也是包体大的原因。
# 三块拼起来的运行时
很多人第一次接触会以为 Electron 和「把 H5 打成 App」的套壳工具一样,其实差别很大。套壳工具一般借用系统自带的 WebView,而 Electron 是自己带了一份完整的 Chromium,再把 Node.js 编译进同一个进程里,两者共用一个 V8。所以不管用户的系统是 Windows 10 还是 macOS 14,你的页面看到的都是同一个版本的内核,前端兼容问题基本没有了,代价是每个应用都背着一个浏览器。
Electron 自己加的那一层是桌面 API:BrowserWindow、Menu、Tray、dialog、Notification、globalShortcut、autoUpdater 这些,大部分只能在主进程用。
# 一个最小应用长什么样
// main.js —— 主进程入口,package.json 里 "main": "main.js"
const { app, BrowserWindow, ipcMain, dialog } = require('electron')
const path = require('node:path')
function createWindow() {
const win = new BrowserWindow({
width: 1000,
height: 700,
webPreferences: {
preload: path.join(__dirname, 'preload.js'),
// 下面三项现在都是默认值,写出来是为了让人一眼看清安全边界
contextIsolation: true,
nodeIntegration: false,
sandbox: true,
},
})
win.loadFile('index.html')
}
// 页面请求打开文件:只在主进程里碰系统能力
ipcMain.handle('dialog:openFile', async () => {
const { canceled, filePaths } = await dialog.showOpenDialog({ properties: ['openFile'] })
return canceled ? null : filePaths[0]
})
app.whenReady().then(() => {
createWindow()
// macOS:点 Dock 图标且没有窗口时重新建一个
app.on('activate', () => {
if (BrowserWindow.getAllWindows().length === 0) createWindow()
})
})
// Windows / Linux 关掉最后一个窗口就退出,macOS 习惯保留在 Dock
app.on('window-all-closed', () => {
if (process.platform !== 'darwin') app.quit()
})
// preload.js —— 在页面脚本之前执行,跑在隔离的上下文里
const { contextBridge, ipcRenderer } = require('electron')
contextBridge.exposeInMainWorld('desktop', {
openFile: () => ipcRenderer.invoke('dialog:openFile'),
})
<!-- index.html -->
<button id="btn">打开文件</button>
<script>
document.getElementById('btn').onclick = async () => {
const file = await window.desktop.openFile()
console.log('选中了', file)
}
</script>
关键点在于页面里只有 window.desktop.openFile 这一个方法,拿不到 ipcRenderer 本身,更拿不到 fs。app.whenReady() 之前不能建窗口,window-all-closed 里按平台区分退出行为,这两处是新手最常漏的。
# 版本上要知道的几个变化
Electron 5 起 nodeIntegration 默认关,Electron 12 起 contextIsolation 默认开,Electron 20 起渲染进程默认进沙箱。网上很多老教程还在写 nodeIntegration: true 然后页面里直接 require('fs'),在新版本上要么跑不起来,要么就是在开安全口子。
面试官追问
追问 1页面里写 require('fs') 报 require is not defined,是 Electron 装坏了吗?
没坏,是新版本默认关了 nodeIntegration、开了沙箱。正确做法是在 preload 里用 contextBridge 暴露需要的方法,别为了省事把 nodeIntegration 打开。
追问 2既然 Node 和 Chromium 在一起,渲染进程里能直接用 Node 吗?
技术上能,开 nodeIntegration 就行,但页面一旦有 XSS 或者加载了第三方内容,攻击者就能直接执行系统命令。我会直接拦,系统能力一律走主进程。
追问 3为什么 macOS 上关掉所有窗口应用不退出?
这是 macOS 的惯例,应用留在 Dock 里,点图标再开窗口。代码里靠 window-all-closed 判断 process.platform !== 'darwin' 才 quit,再配合 activate 事件重建窗口。
追问 4Electron 和 Cordova 这种套壳方案最大的区别是什么?
Cordova 用系统 WebView,内核版本跟着系统走;Electron 自带固定版本的 Chromium,并且有完整的 Node 主进程。前者包小但兼容要自己扛,后者包大但环境完全可控。
追问 5主进程能不能直接操作页面的 DOM?
不能,DOM 在渲染进程里。主进程只能通过 webContents.send 发消息、或者 executeJavaScript 注入脚本,后者尽量少用,等于绕开了 preload 定好的接口。
# 做桌面端,Electron、Tauri、NW.js 和原生你怎么选?
30 秒速记
- 先问清楚:团队是不是前端为主、包体有没有硬指标、要多深的系统集成、要不要跨三端一致
Electron:自带Chromium,渲染一致、生态最全(VS Code、Slack、飞书桌面端都是),代价是包大、内存高Tauri:系统WebView+Rust后端,包体几MB,但三端内核不一样,兼容问题回来了NW.js:Node和DOM在同一个上下文,上手快但安全隔离弱,社区比Electron小很多- 原生(
Swift/C#/Qt):性能和系统集成最好,三端要写三份或学C++,前端团队接不住
我会先看团队和硬约束,而不是先比谁包小。 前端团队做复杂业务界面、要求三端渲染一致,选 Electron 最稳,生态和踩坑资料最多。包体或内存有硬指标,界面也不算复杂,可以考虑 Tauri,但要接受 Windows 走 WebView2、macOS 走
