桌面应用:Electron / Tauri
桌面客户端的现代主流是「Web 技术做界面 + 壳负责系统能力」:Electron 自带 Chromium,Tauri 用系统 WebView。本页先给方案全景,再分别讲两条路线的模型、安全基线与分发,避免「界面写得爽、壳上踩大坑」。
桌面方案全景
| 方案 | 界面 | 内核/运行 | 体积与内存 | 适合 |
|---|---|---|---|---|
| Electron | Web(Chromium) | Node + Chromium | 大 | 成熟产品、需要 Node 生态(VS Code、Slack 系) |
| Tauri | Web | 系统 WebView + Rust | 小 | 轻量工具、团队会 Rust、重安全 |
| Flutter Desktop | Flutter | 自绘引擎 | 中 | 想与移动端共用一套 Flutter 代码 |
| Qt / 原生 | Qt / 系统 API | 原生 | 小 | 重系统集成、传统桌面 |
| Wails | Web | 系统 WebView + Go | 小 | Go 团队轻量方案 |
选型已放在技术全景与选型,本页聚焦实现层。
Electron:主进程与渲染进程
进程模型是一切认知的基础:
| 进程 | 职责 | 能力边界 |
|---|---|---|
| 主进程(Node) | 窗口管理、系统能力、生命周期 | 可访问 Node 与系统 API |
| 渲染进程 | 页面 UI(Chromium) | 默认被沙箱限制,不直接拥有 Node 全量能力 |
| preload 脚本 | 桥接主/渲染 | 在渲染进程加载前执行,暴露受控 API |
安全基线(顺序重要,见「安全红线」节):
contextIsolation: true(渲染进程与 preload 隔离);nodeIntegration: false(渲染进程不开 Node);sandbox: true;- 用
contextBridge.exposeInMainWorld只暴露白名单 API; - IPC 用
ipcRenderer.invoke+ipcMain.handle(异步请求应答模式),少用send/on全局事件。
ts
// preload.ts —— 只暴露两个最小能力
import { contextBridge, ipcRenderer } from 'electron'
contextBridge.exposeInMainWorld('app', {
getVersion: () => ipcRenderer.invoke('app:get-version'),
openExternal: (url: string) => ipcRenderer.invoke('app:open-external', url)
})ts
// main.ts
import { app, BrowserWindow, ipcMain, shell } from 'electron'
app.whenReady().then(() => {
const win = new BrowserWindow({
width: 1024, height: 720,
webPreferences: { contextIsolation: true, nodeIntegration: false, sandbox: true, preload: PRELOAD_PATH }
})
})
ipcMain.handle('app:get-version', () => app.getVersion())
ipcMain.handle('app:open-external', (_e, url: string) => shell.openExternal(url))渲染进程安全红线
| 红线 | 说明 |
|---|---|
| 绝不开 nodeIntegration | 渲染页面一旦有 XSS,等于直接 RCE |
| preload 只暴露最小面 | 不要 exposeInMainWorld('api', ipcRenderer) 整对象 |
| 渲染进程不加载远程 JS | 主窗口只加载本地文件或受控远程内容 |
| CSP 生效 | 给 HTML 加 CSP,限制 script-src |
| 校验 IPC 参数 | handle 里对来自渲染的参数做白名单/类型校验(渲染可能被攻破) |
| 外链必须走白名单 | openExternal 只允许协议白名单 |
这些纪律在 Electron 里不是「最佳实践」而是「安全下限」——渲染进程是不可信输入。
Electron 常用能力
- 单实例锁:
app.requestSingleInstanceLock(),防止开多个实例; - 托盘
Tray、系统菜单Menu、全局快捷键globalShortcut; - 设置持久化:
electron-store;系统对话框dialog;文件访问fs/协议; - 自动更新:
electron-updater(配合打包产物与更新服务,macOS 需签名); - 窗口事件:
ready-to-show再显示避免白屏闪烁。
Tauri 2:Rust 内核 + 系统 WebView
Tauri 2(2.x 稳定线)理念:能 Web 解决的归 Web,要系统能力的归 Rust。
- 界面:前端(任意框架,本地打包或远端可配)渲染到系统 WebView;
- 逻辑:
#[tauri::command]暴露给前端调用;编译产物是原生二进制,无 Node 运行时; - 权限模型:
capabilities(ACL)——前端能调哪些命令、读哪些路径,要在tauri.conf.json与 capabilities 文件显式声明,未授权调用直接拒绝; - 插件体系:官方插件(store、dialog、fs、updater、notification)与社区插件都走同一授权模型;
- 配置:
tauri.conf.json管理窗口、构建、打包、bundle 标识; - 多窗口与事件:
WebviewWindow、emit/listen事件总线。
rust
// main.rs 侧暴露一个命令
#[tauri::command]
fn greet(name: String) -> String {
format!("Hello, {}!", name)
}
// 注册命令
tauri::Builder::default()
.invoke_handler(tauri::generate_handler![greet])
.run(tauri::generate_context!())
.expect("error while running tauri application");前端(若用 JS 侧 SDK):
js
import { invoke } from '@tauri-apps/api/core'
const msg = await invoke('greet', { name: 'Rust' })安全注意:即使前端被 XSS 攻破,能力也被 capabilities 白名单兜底——这正是 Tauri 把安全做进框架的方式。
Tauri 常用能力
- 系统 WebView 无 Chromium 体积负担:安装包典型个位数到十几 MB;
- updater 更新需公钥签名(Tauri 默认要求 updater 签名),CI 里保管私钥;
- 跨平台构建注意:macOS 打包要在 macOS 上(签名/公证),Windows 亦然,CI 用对应 runner;
- 与 Rust 生态互操作强(文件处理、本地服务、加密等都可走 Rust crate)。
打包、签名与自动更新
两条路线共用的底座:
- 代码签名是桌面发布的前提:macOS 需 Developer ID 签名 + notarization(公证),否则 Gatekeeper 拦下载;Windows 需代码签名证书避免 SmartScreen 惊吓;Linux 走 deb/rpm/AppImage 与 Flatpak/AppArmor 语境;
- 自动更新与签名强绑定:Electron 用 electron-updater;Tauri 用官方 updater(签名公钥放前端配置);更新失败要有回退(启动自检 + 下载失败提示);
- 安装包与商店:macOS App Store、Microsoft Store 有各自沙箱与审核约束,与官网直发路线并存;
- CI 里构建矩阵按系统跑,签名私钥/公证凭据放 secret 仓库(详见打包、签名与发布 与容器与部署的 CI 一节)。
常见坑速查
| 坑 | 现象 | 对策 |
|---|---|---|
| Electron 开着 nodeIntegration | 渲染页一点 XSS 就整机 RCE | 默认全关 + contextIsolation + preload 白名单 |
| preload 把 ipcRenderer 整包暴露 | 渲染进程可发任意命令 | 只 expose 封装好的最小方法 |
| IPC 参数不校验 | 主进程被伪造请求打穿 | handle 里白名单校验,别信任渲染进程 |
| 窗口白屏闪烁 | 冷启动视觉差 | ready-to-show 后再 show;用背景色兜底 |
| 多开 | 用户拖出多个主窗口/托盘混乱 | 单实例锁 + 二次激活聚焦主窗口 |
| Tauri 未授权调用 | 前端报权限拒绝却不知为何 | 在 capabilities 文件补 scope/命令声明 |
| 更新包未签名 | updater 校验失败或系统拦截 | Tauri 配签名;Electron 关 macOS 弹窗需签 |
| 忘公证 | macOS 用户「已损坏,打不开」 | notarization + staple,CI 脚本化 |
| 升级把用户配置弄丢 | 大版本 schema 变了崩溃 | 迁移版本化:settings schema 带版本号 |
| 只在本机测 | 不同系统表现差异(字体/DPI/路径) | CI 三系统构建 + 冒烟清单 |
检查清单
- [ ] 已选定主从/前后端架构与权限模型,安全基线全开;
- [ ] 渲染进程不可信原则贯穿(preload 白名单 + IPC 校验 + CSP);
- [ ] 自动更新带签名与失败回退;
- [ ] 三系统(Win/macOS/Linux)构建与冒烟清单齐备;
- [ ] 配置持久化带版本迁移,升级不丢用户数据;
- [ ] 单实例、托盘、深链等桌面习惯做了。