Published on

如何设计和管理好一个Electron应用?

Authors
  • avatar
    Name
    narclee
    Twitter

设计和管理 Electron 应用,要控制好这几个边界:

  1. 浏览器进程边界:Main / Renderer / Preload 怎么分工。
  2. 权限边界:谁可以访问 Node、文件系统、系统 API。
  3. 产品边界:哪些能力真的需要桌面能力,哪些只是 Web 能力。
  4. 工程边界:构建、升级、崩溃、日志、性能、数据迁移怎么长期治理。

Electron 本质上是 Chromium + Node.js + 原生系统能力的组合,跨平台是它的优势,但它也把 Web XSS、Node RCE、桌面权限滥用、包体膨胀、内存占用等问题一起带进来了。

Electron 官方也明确把应用拆成 Main process 和 Renderer process;Main 更像应用控制器,Renderer 更像网页渲染进程。


一、先建立正确的心智模型

不能用Web开发的思路做客户端的工程,应该建立的模型是:

  • Main Process:桌面应用内核
  • Preload Layer:安全桥接层 / 最小能力暴露
  • Renderer Process:UI Web App
  • Worker / Utility:重任务、后台任务、AI/索引/文件扫描等
  • Local Data Layer:SQLite / 文件 / IndexedDB / 配置 / 缓存
  • Update Layer:自动更新 / 版本迁移 / 回滚
  • Observability:日志 / 崩溃 / 性能 / 行为追踪

二、进程职责怎么拆

1. Main Process:应用内核,不写 UI 业务

Main process 负责:

窗口生命周期
菜单 / Dock / Tray
系统权限
文件系统
自动更新
协议注册
快捷键
IPC Handler
本地数据库连接
后台任务调度
崩溃恢复

它不应该负责:

复杂 UI 状态
页面业务逻辑
React/Vue store
DOM 操作
表单逻辑
普通前端请求状态

可以把 Main 理解成一个桌面端BFF,或者一个本地操作系统适配层。

推荐结构:

src/main/
├── app.ts                 # app ready / quit / lifecycle
├── windows/
│   ├── mainWindow.ts
│   ├── settingsWindow.ts
│   └── windowManager.ts
├── ipc/
│   ├── file.ipc.ts
│   ├── auth.ipc.ts
│   ├── update.ipc.ts
│   └── index.ts
├── services/
│   ├── fileService.ts
│   ├── dbService.ts
│   ├── updateService.ts
│   ├── shellService.ts
│   └── telemetryService.ts
├── security/
│   ├── permissions.ts
│   ├── csp.ts
│   └── navigationGuard.ts
└── main.ts

Main 的设计原则是:薄入口,厚 service,IPC handler 只做参数校验和 service 调用。


2. Renderer Process:纯 Web UI

Renderer 负责:

页面路由
组件渲染
用户交互
表单状态
前端缓存
业务展示逻辑
编辑器 / Canvas / 图表 / Timeline

Renderer 不应该知道 Electron 的太多细节。理想情况下,你的 Renderer 大部分代码可以当成一个普通 Web App 跑起来。

推荐结构:

src/renderer/
├── app/
├── pages/
├── components/
├── features/
├── stores/
├── hooks/
├── api/
│   ├── desktopClient.ts
│   └── types.ts
└── main.tsx

Renderer 只通过一个抽象客户端访问桌面能力:

await desktop.file.pickFile()
await desktop.file.readText(fileId)
await desktop.update.check()
await desktop.system.openExternal(url)

不要在业务组件里到处写:

window.electron.ipcRenderer.invoke(...)

这会让桌面能力污染整个前端应用。


3. Preload Layer:最关键的安全边界

Preload 是 Electron 应用最容易被低估的部分。

官方文档也强调,preload 在页面加载前执行,可以访问 DOM 和 Node 环境,常用于通过 contextBridge 向 renderer 暴露受控 API。

正确做法:

// preload.ts
import { contextBridge, ipcRenderer } from 'electron'

contextBridge.exposeInMainWorld('desktop', {
  file: {
    pickFile: () => ipcRenderer.invoke('file:pick'),
    readText: (fileId: string) => ipcRenderer.invoke('file:readText', fileId),
  },
  update: {
    check: () => ipcRenderer.invoke('update:check'),
  },
})

Renderer 看到的是:

window.desktop.file.pickFile()

不是:

window.require('fs')
window.ipcRenderer
window.process

Preload 的设计原则是:

只暴露白名单能力
不暴露 ipcRenderer 原对象
不暴露 Node global
不暴露通用 execute / eval / shell API
所有参数必须校验
所有返回值必须类型化

三、安全设计是 Electron 的第一优先级

Electron 安全的底线配置:

new BrowserWindow({
  webPreferences: {
    preload: preloadPath,
    nodeIntegration: false,
    contextIsolation: true,
    sandbox: true,
    webSecurity: true,
  },
})

Electron 官方安全文档强调了 context isolation、sandbox、禁用不必要 Node 能力等建议;并且 Electron 20 起 renderer sandbox 已经默认启用,但如果关闭 context isolation,会影响沙箱能力。(Electron)

你可以把 Electron 安全问题浓缩成一句话:

在普通 Web 应用里,XSS 可能是账号风险;
Electron 应用里,XSS 可能升级成 RCE

所以要管住这些东西:

nodeIntegration: false
contextIsolation: true
sandbox: true
disable remote module
不加载不可信远程页面
不允许任意导航
不允许任意 window.open
外链必须 shell.openExternal 且校验 URL
IPC 必须白名单
CSP 必须严格
preload 不泄露能力

窗口导航要拦:

win.webContents.setWindowOpenHandler(({ url }) => {
  if (isSafeExternalUrl(url)) {
    shell.openExternal(url)
  }
  return { action: 'deny' }
})

win.webContents.on('will-navigate', (event, url) => {
  if (!isAllowedNavigation(url)) {
    event.preventDefault()
  }
})

IPC 也要当成 API 网关,不要这样:

ipcMain.handle('fs:read', (_, path) => fs.readFile(path, 'utf-8'))

而应该这样:

ipcMain.handle('file:readText', async (_, fileId: string) => {
  const input = FileReadSchema.parse({ fileId })
  return fileService.readUserSelectedTextFile(input.fileId)
})

也就是说,Renderer 不能传任意路径。它只能传你系统内部认可的 fileIdworkspaceIddocumentId


四、IPC 要设计成内部 API,而不是事件乱飞

很多 Electron 的管理混乱,根源是 IPC 没有治理。

ipcMain.handle / ipcRenderer.invoke 一旦到处散落,应用会变成“跨进程 callback 地狱”。

推荐把 IPC 当 RPC 设计:

channel 命名规范
请求类型
响应类型
错误模型
权限校验
日志追踪
超时控制
幂等性
版本兼容

示例:

file.pick
file.readText
file.writeText
db.query
update.check
update.install
auth.login
window.minimize
system.openExternal

不要:

send-message
do-action
run-command
handle-event
main-event

类型设计:

type DesktopAPI = {
  file: {
    pickFile(): Promise<PickFileResult>
    readText(input: { fileId: string }): Promise<{ content: string }>
  }
  update: {
    check(): Promise<UpdateCheckResult>
  }
}

错误模型也要统一:

type DesktopError = {
  code:
    | 'PERMISSION_DENIED'
    | 'FILE_NOT_FOUND'
    | 'INVALID_ARGUMENT'
    | 'SYSTEM_ERROR'
    | 'UPDATE_FAILED'
  message: string
  detail?: unknown
}

Renderer 不应该直接依赖 Electron error stack,而应该拿到稳定的业务错误。


五、本地数据层要认真设计

Electron 应用经常有本地数据:

用户配置
登录态
缓存
离线数据
工作区数据
索引数据
草稿
本地数据库
下载文件
日志

不要随便把所有东西塞进 localStorage。严肃一点可以分层:

配置:app config JSON
敏感信息:系统 Keychain / Credential Manager
结构化数据:SQLite
大文件:app data directory
临时缓存:cache directory
前端轻状态:IndexedDB / localStorage

推荐原则:

用户数据和缓存分开
业务数据和日志分开
敏感数据和普通配置分开
可重建数据和不可重建数据分开

目录大概是:

userData/
├── config.json
├── app.db
├── workspaces/
├── cache/
├── logs/
├── crash/
└── migrations/
从旧版本迁移
迁移失败回滚
备份数据库
schema version
数据修复脚本

这是很多桌面应用比 Web 应用更麻烦的地方:用户本地数据一旦损坏,你没有中心化 DB 可以直接修。


六、窗口系统要有 WindowManager

不要在业务代码里到处创建窗口。应该集中管理:

class WindowManager {
  private mainWindow?: BrowserWindow
  private settingsWindow?: BrowserWindow

  createMainWindow() {}
  getMainWindow() {}
  showSettingsWindow() {}
  broadcast(channel: string, payload: unknown) {}
  closeAll() {}
}

窗口要管理:

创建
显示
隐藏
聚焦
恢复
多窗口通信
窗口状态记忆
崩溃恢复
深链打开
单实例锁

典型能力:

const gotLock = app.requestSingleInstanceLock()

if (!gotLock) {
  app.quit()
} else {
  app.on('second-instance', (_, argv) => {
    windowManager.focusMainWindow()
    deepLinkService.handleArgv(argv)
  })
}

七、后台任务不要阻塞 Main 和 Renderer

Electron 应用里常见重任务:

文件扫描
代码索引
全文搜索
AI 推理 / 调模型
图片处理
PDF 解析
Git 操作
压缩解压
数据库批处理

不要直接扔到 Renderer,也不要把 Main 卡死。

可以用:

Node worker_threads
Electron utilityProcess
child_process
独立本地服务
Web Worker

设计上可以分为:

UI thread:只渲染
Main process:调度和权限
Worker:执行重任务
Database:持久化
EventBus:进度上报

比如:

Renderer -> IPC -> Main -> TaskManager -> Worker
Renderer <- progress event <- Main <- Worker

任务模型最好统一:

type Task = {
  id: string
  type: 'index-workspace' | 'parse-pdf' | 'sync-data'
  status: 'pending' | 'running' | 'succeeded' | 'failed' | 'cancelled'
  progress: number
  error?: DesktopError
}

八、自动更新是产品级能力,不是附加功能

Electron 应用要认真处理更新:

检查更新
静默下载
用户确认安装
强制更新
灰度更新
增量更新
失败回滚
版本兼容
数据库迁移
更新日志
签名校验

不要只接一个 autoUpdater.checkForUpdates() 就完事。更新策略要区分:

普通功能更新:可选
安全更新:强提示
协议/数据不兼容更新:强制
灰度更新:分批

版本升级时要先问:

这个版本是否需要 DB migration?
是否兼容旧数据?
是否兼容旧插件?
是否兼容旧配置?
失败后能不能继续打开旧版本?

九、工程化结构推荐

如果你用 React / Vue / Nuxt 风格都可以,但 Electron 工程建议天然按进程分包:

apps/desktop/
├── src/
│   ├── main/
│   ├── preload/
│   ├── renderer/
│   └── shared/
├── electron.vite.config.ts
├── package.json
└── resources/

或者 monorepo:

packages/
├── desktop-main/
├── desktop-preload/
├── desktop-renderer/
├── desktop-shared/
├── desktop-native/
└── ui/

关键是 shared 只能放纯类型和纯逻辑:

DTO types
schema
constants
error codes
pure utils

不要让 shared 同时依赖 Node 和 DOM,否则打包会变脏。


十、构建、签名、发布要一开始就设计清楚

Electron 跨平台不是“写一次直接发三端”。你要管理:

macOS: dmg / zip / notarization / code signing
Windows: nsis / msi / code signing
Linux: AppImage / deb / rpm

还要考虑:

x64 / arm64
自动更新源
CI 构建环境
证书管理
产物校验
release channel
beta / stable
nightly

推荐 release channel:

dev      本地开发
alpha    内部验证
beta     小范围用户
stable   正式用户

版本语义:

1.4.0
1.4.1
1.5.0-beta.1
1.5.0-alpha.3

构建配置要做到:

本地 dev 能跑
CI 能打包
不同平台配置隔离
不同 channel 更新源隔离

十一、日志和可观测性必须有

桌面应用最痛的是:用户机器上的问题你看不见。

所以一开始就要有:

main process log
renderer log
preload log
worker log
crash report
performance metrics
update log
IPC error log
database migration log

日志目录:

logs/
├── main.log
├── renderer.log
├── update.log
├── crash.log
└── worker.log

每个 IPC 调用最好能记录:

channel
requestId
duration
success / failure
error code
app version
platform

不要记录敏感内容,例如用户文件内容、token、隐私路径等。

崩溃治理要考虑:

Renderer crashed 后自动 reload?
Main 异常怎么退出?
Worker 崩了是否重启?
更新失败是否回滚?
DB migration 失败是否恢复备份?

十二、性能管理

Electron 性能问题主要来自:

启动慢
包体大
内存高
Renderer 卡顿
IPC 频繁
大数据渲染
后台任务阻塞
Chromium 多进程占用

治理原则:

首屏最小化
懒加载 heavy module
避免启动时初始化所有服务
大任务丢 worker
大列表虚拟化
IPC 批处理
本地缓存分层
避免 Renderer 持有超大对象

启动阶段可以拆:

Phase 1: app ready
Phase 2: create window
Phase 3: show skeleton
Phase 4: load user session
Phase 5: lazy init background services
Phase 6: warm cache / check update

不要在启动时做:

扫描全盘
初始化大模型
建立所有索引
DB 全量 migration
读取巨大配置
加载全部插件

十三、权限与能力系统

如果你的 Electron 应用未来会有插件、Agent、脚本、自动化能力,必须设计 capability model。

比如:

type Capability =
  | 'file:read'
  | 'file:write'
  | 'shell:openExternal'
  | 'git:read'
  | 'network:request'
  | 'clipboard:read'
  | 'clipboard:write'

每次敏感操作都经过:

调用方是谁?
有没有权限?
用户是否授权?
作用域是什么?
是否可审计?
是否可撤销?
Agent 不能默认拿全盘权限
Agent 不能默认执行 shell
Agent 不能默认读 SSH key
Agent 不能默认读浏览器 cookie
Agent 不能默认访问任意目录

应该做 workspace-scoped permission:

只允许访问用户选择的 workspace
只允许执行白名单命令
高危命令二次确认
命令执行有日志
输出流可追踪
任务可取消

十四、推荐的核心架构图

┌───────────────────────────────────────┐
Renderer: React/Vue UI- Pages / Components / Stores- No Node.js- Calls window.desktop.*└───────────────────┬───────────────────┘
                    │ typed API
┌───────────────────▼───────────────────┐
Preload: Secure Bridge- contextBridge                        │
- whitelist APIs- no raw ipcRenderer exposure          │
└───────────────────┬───────────────────┘
IPC
┌───────────────────▼───────────────────┐
Main Process: App Kernel- WindowManager- IPC Router- Permission Manager- Update Manager- Task Manager- DB / File Services└─────────────┬───────────────┬─────────┘
              │               │
       ┌──────▼──────┐ ┌──────▼──────┐
Local Data  │ │ WorkersSQLite/File │ │ Heavy Tasks       └─────────────┘ └─────────────┘

十五、管理 Electron 项目的 Checklist

架构层

Main / Renderer / Preload 是否严格分层?
Renderer 是否完全禁用 Node?
IPC 是否类型化、白名单化?
是否有统一 WindowManager?
是否有统一 TaskManager?
是否有统一 Error Model?

安全层

nodeIntegration=falsecontextIsolation=truesandbox=true是否禁止任意导航?
是否限制 window.open?
是否有 CSPPreload 是否没有暴露 ipcRenderer?
外部链接是否校验?
文件访问是否基于用户授权?

数据层

是否区分 config / cache / user data / logs?
是否有 DB migration?
是否有 migration backup?
敏感信息是否进 Keychain?
本地数据损坏是否能恢复?

工程层

是否支持 dev / beta / stable?
是否有 macOS / Windows / Linux CI是否有代码签名?
是否有自动更新?
是否有 release notes?
是否有 crash log?

运行层

启动耗时是否可观测?
IPC 耗时是否可观测?
Renderer crash 是否可恢复?
Worker crash 是否可恢复?
更新失败是否可诊断?
用户问题是否能导出诊断包?