Electron 实操开发文档

技术栈:TypeScript + React + electron-vite

目标平台:Windows / macOS / Linux

深度:从装环境到打包出可分发的安装包


1. Electron 是什么

Electron 把 Chromium(渲染) 和 Node.js(后端) 打包进同一个可执行文件,让你用 Web 技术写桌面应用。

scss 复制代码
┌─────────────────────────────────────┐
│            你的 Electron App         │
│  ┌────────────┐  ┌────────────────┐ │
│  │  主进程     │  │  渲染进程       │ │
│  │ (Node.js)  │◄─┤ (Chromium)    │ │
│  │ 文件/系统   │  │ React UI      │ │
│  └────────────┘  └────────────────┘ │
│       ↑ Preload Script (桥接)        │
└─────────────────────────────────────┘

优势 :上手最低门槛,前端工程师零学习曲线,生态最成熟(VS Code、Slack、Discord 都用它)。
劣势:安装包大(100 MB 起),内存占用高,每个窗口是一个完整的 Chrome 实例。


2. 环境准备

2.1 所有平台通用

依赖 版本要求 安装
Node.js ≥ 20 LTS nodejs.org 或用 fnm/nvm 管理
pnpm ≥ 8 npm install -g pnpm
Git 任意 git-scm.com

验证:

bash 复制代码
node -v     # v20.x
pnpm -v     # 8.x
git --version

2.2 Windows

安装 Visual Studio Build Tools 2022(原生 Node 模块编译用):

  1. 下载 vs_BuildTools.exe
  2. 勾选 "使用 C++ 的桌面开发"
  3. 安装完重启终端

设置 Electron 下载镜像(国内必做,否则 pnpm install 卡死):

bash 复制代码
# 项目根目录创建 .npmrc,或全局 ~/.npmrc
ELECTRON_MIRROR=https://npmmirror.com/mirrors/electron/

2.3 macOS

bash 复制代码
xcode-select --install    # 安装命令行工具,会弹 GUI 确认框

如果要打公证的 .dmg(用户安装不会报"已损坏"),需要 Apple Developer 账号($99/年),见第 8 章。

2.4 Linux (Ubuntu / Debian)

bash 复制代码
sudo apt update
sudo apt install -y \
  build-essential \
  libnss3 \
  libatk-bridge2.0-0 \
  libgtk-3-0 \
  libgbm1 \
  libasound2

3. 创建项目

使用 electron-vite 脚手架(Vite + React + TS 集成最好的选择):

bash 复制代码
pnpm create @quick-start/electron my-app

交互选项:

sql 复制代码
✔ Select a framework › React
✔ Add TypeScript? › Yes
✔ Add ESLint? › Yes

进入项目:

bash 复制代码
cd my-app
pnpm install
pnpm dev

第一次运行会下载 Electron 二进制(约 80 MB),下载完后会弹出一个应用窗口,支持热更新。

3.1 目录结构解读

perl 复制代码
my-app/
├── src/
│   ├── main/               # 主进程(Node 环境)
│   │   └── index.ts        ← 应用入口,创建窗口、注册 IPC
│   ├── preload/            # 预加载脚本(桥接层)
│   │   └── index.ts        ← 唯一可以同时访问 Node + DOM 的地方
│   └── renderer/           # 渲染进程(浏览器环境)
│       └── src/
│           ├── App.tsx     ← React UI 入口
│           └── main.tsx
├── electron.vite.config.ts  # Vite 构建配置(三进程各一份)
├── electron-builder.yml     # 打包配置
└── package.json

4. 三进程模型(核心概念)

Electron 的安全模型是理解一切的基础,搞错这里会埋下严重漏洞。

4.1 主进程(Main Process)

  • 环境:Node.js,有完整文件系统/网络/子进程权限
  • 职责:创建/管理窗口,响应系统事件,处理菜单、托盘
  • 数量:整个应用唯一一个
ts 复制代码
// src/main/index.ts
import { app, BrowserWindow } from 'electron'
import path from 'node:path'

function createWindow() {
  const win = new BrowserWindow({
    width: 900,
    height: 600,
    webPreferences: {
      preload: path.join(__dirname, '../preload/index.js'),
      contextIsolation: true,   // 必须 true!
      nodeIntegration: false,   // 必须 false!
    },
  })
  
  if (process.env.NODE_ENV === 'development') {
    win.loadURL('http://localhost:5173')
    win.webContents.openDevTools()
  } else {
    win.loadFile(path.join(__dirname, '../renderer/index.html'))
  }
}

app.whenReady().then(createWindow)

4.2 渲染进程(Renderer Process)

  • 环境 :Chromium(浏览器),默认不能访问 Node.js
  • 职责:跑 React UI
  • 数量:每个窗口一个

渲染进程就是标准的 React 应用,和网页开发完全一样,唯一不同是可以通过 preload 暴露的 API 调用系统能力。

4.3 预加载脚本(Preload Script)

  • 环境 :同时能访问 Node.js API 和 window 对象
  • 职责:把主进程能力安全地暴露给渲染进程(白名单机制)
  • 关键 API :contextBridge.exposeInMainWorld
ts 复制代码
// src/preload/index.ts
import { contextBridge, ipcRenderer } from 'electron'

contextBridge.exposeInMainWorld('electronAPI', {
  // 渲染进程只能调用你在这里列出的东西
  readFile: (p: string) => ipcRenderer.invoke('fs:readFile', p),
  writeFile: (p: string, data: string) => ipcRenderer.invoke('fs:writeFile', p, data),
  onThemeChange: (cb: (theme: string) => void) => {
    ipcRenderer.on('theme:changed', (_e, theme) => cb(theme))
  },
})

4.4 IPC 通信

IPC(进程间通信)是主进程和渲染进程交换数据的机制。

scss 复制代码
渲染进程                     主进程
  │                            │
  │  ipcRenderer.invoke()      │
  │ ─────────────────────────► │  ipcMain.handle()
  │                            │  执行 Node 操作
  │ ◄───────────────────────── │
  │  返回结果 (Promise)         │

主进程注册处理器:

ts 复制代码
// src/main/index.ts
import { ipcMain, app } from 'electron'
import { readFile, writeFile } from 'node:fs/promises'
import path from 'node:path'

ipcMain.handle('fs:readFile', async (_event, filename: string) => {
  // ⚠ 安全:永远校验路径,防止路径穿越攻击
  const userData = app.getPath('userData')
  const safePath = path.resolve(userData, filename)
  if (!safePath.startsWith(userData)) {
    throw new Error('Access denied: path traversal detected')
  }
  return await readFile(safePath, 'utf-8')
})

ipcMain.handle('fs:writeFile', async (_event, filename: string, data: string) => {
  const userData = app.getPath('userData')
  const safePath = path.resolve(userData, filename)
  if (!safePath.startsWith(userData)) {
    throw new Error('Access denied')
  }
  await writeFile(safePath, data, 'utf-8')
})

5. 完整实战示例:本地 Note 应用

5.1 目标功能

  • 输入框写笔记
  • 保存到本地文件(userData 目录)
  • 下次启动自动加载

5.2 主进程(完整)

ts 复制代码
// src/main/index.ts
import { app, BrowserWindow, ipcMain } from 'electron'
import { readFile, writeFile, mkdir } from 'node:fs/promises'
import path from 'node:path'

const NOTES_DIR = () => path.join(app.getPath('userData'), 'notes')

async function ensureNotesDir() {
  await mkdir(NOTES_DIR(), { recursive: true })
}

ipcMain.handle('notes:load', async (_e, name: string) => {
  await ensureNotesDir()
  const file = path.join(NOTES_DIR(), `${name}.txt`)
  try {
    return await readFile(file, 'utf-8')
  } catch {
    return ''   // 文件不存在时返回空字符串
  }
})

ipcMain.handle('notes:save', async (_e, name: string, content: string) => {
  await ensureNotesDir()
  const file = path.join(NOTES_DIR(), `${name}.txt`)
  await writeFile(file, content, 'utf-8')
})

function createWindow() {
  const win = new BrowserWindow({
    width: 800,
    height: 600,
    webPreferences: {
      preload: path.join(__dirname, '../preload/index.js'),
      contextIsolation: true,
      nodeIntegration: false,
    },
  })
  if (process.env.NODE_ENV === 'development') {
    win.loadURL('http://localhost:5173')
  } else {
    win.loadFile(path.join(__dirname, '../renderer/index.html'))
  }
}

app.whenReady().then(createWindow)
app.on('window-all-closed', () => { if (process.platform !== 'darwin') app.quit() })
app.on('activate', () => { if (BrowserWindow.getAllWindows().length === 0) createWindow() })

5.3 预加载脚本(完整)

ts 复制代码
// src/preload/index.ts
import { contextBridge, ipcRenderer } from 'electron'

contextBridge.exposeInMainWorld('notes', {
  load: (name: string): Promise<string> => ipcRenderer.invoke('notes:load', name),
  save: (name: string, content: string): Promise<void> =>
    ipcRenderer.invoke('notes:save', name, content),
})

5.4 React UI(完整)

tsx 复制代码
// src/renderer/src/App.tsx
import { useState, useEffect } from 'react'

// 告诉 TS window.notes 的类型
declare global {
  interface Window {
    notes: {
      load: (name: string) => Promise<string>
      save: (name: string, content: string) => Promise<void>
    }
  }
}

const NOTE_NAME = 'default'

export default function App() {
  const [text, setText] = useState('')
  const [saved, setSaved] = useState(true)

  useEffect(() => {
    window.notes.load(NOTE_NAME).then(setText)
  }, [])

  const handleSave = async () => {
    await window.notes.save(NOTE_NAME, text)
    setSaved(true)
  }

  return (
    <div style={{ padding: 20, fontFamily: 'sans-serif' }}>
      <h2>My Notes {!saved && '●'}</h2>
      <textarea
        value={text}
        onChange={(e) => { setText(e.target.value); setSaved(false) }}
        style={{ width: '100%', height: 300, fontSize: 14 }}
      />
      <br />
      <button onClick={handleSave} disabled={saved}>
        {saved ? 'Saved' : 'Save (Ctrl+S)'}
      </button>
    </div>
  )
}

运行 pnpm dev,改内容保存后关窗口重开,数据会保留。


6. 系统集成

6.1 系统托盘

ts 复制代码
// src/main/index.ts 里加
import { Tray, Menu, nativeImage } from 'electron'
import path from 'node:path'

let tray: Tray | null = null

function createTray() {
  const icon = nativeImage.createFromPath(
    path.join(__dirname, '../../resources/tray-icon.png')
  )
  tray = new Tray(icon.resize({ width: 16, height: 16 }))
  tray.setContextMenu(Menu.buildFromTemplate([
    { label: '显示窗口', click: () => win?.show() },
    { type: 'separator' },
    { label: '退出', click: () => app.quit() },
  ]))
  tray.setToolTip('My App')
}

6.2 原生菜单

ts 复制代码
import { Menu } from 'electron'

const menuTemplate: Electron.MenuItemConstructorOptions[] = [
  {
    label: '文件',
    submenu: [
      { label: '保存', accelerator: 'CmdOrCtrl+S', click: () => win?.webContents.send('menu:save') },
      { type: 'separator' },
      { label: '退出', role: 'quit' },
    ],
  },
  { label: '编辑', role: 'editMenu' },
  { label: '视图', role: 'viewMenu' },
]

Menu.setApplicationMenu(Menu.buildFromTemplate(menuTemplate))

6.3 系统对话框(打开/保存文件)

ts 复制代码
import { dialog } from 'electron'

ipcMain.handle('dialog:openFile', async () => {
  const result = await dialog.showOpenDialog({
    properties: ['openFile'],
    filters: [{ name: 'Text', extensions: ['txt', 'md'] }],
  })
  return result.canceled ? null : result.filePaths[0]
})

ipcMain.handle('dialog:saveFile', async (_e, content: string) => {
  const result = await dialog.showSaveDialog({
    filters: [{ name: 'Text', extensions: ['txt'] }],
  })
  if (!result.canceled && result.filePath) {
    await writeFile(result.filePath, content, 'utf-8')
    return result.filePath
  }
  return null
})

6.4 Shell 操作

ts 复制代码
import { shell } from 'electron'

// 用系统默认浏览器打开链接(不要在 webview 里直接导航到外部链接)
ipcMain.handle('shell:openExternal', (_e, url: string) => {
  shell.openExternal(url)
})

// 在文件管理器里显示文件
ipcMain.handle('shell:showInFolder', (_e, filePath: string) => {
  shell.showItemInFolder(filePath)
})

7. 调试

7.1 渲染进程调试

开发模式下主进程会自动打开 DevTools(见 win.webContents.openDevTools()),和 Chrome DevTools 完全一样。

7.2 主进程调试

VS Code 配置 .vscode/launch.json:

json 复制代码
{
  "version": "0.2.0",
  "configurations": [
    {
      "name": "Debug Main Process",
      "type": "node",
      "request": "launch",
      "runtimeExecutable": "${workspaceRoot}/node_modules/.bin/electron",
      "args": [".", "--inspect=5858"],
      "windows": {
        "runtimeExecutable": "${workspaceRoot}/node_modules/.bin/electron.cmd"
      }
    }
  ]
}

或者直接在终端加 --inspect:

bash 复制代码
ELECTRON_INSPECT=5858 pnpm dev
# 然后在 Chrome 开 chrome://inspect

8. 打包

8.1 electron-builder 配置

electron-builder.yml(脚手架已生成,按需修改):

yaml 复制代码
appId: com.yourcompany.myapp          # 唯一标识符,反向域名格式
productName: My App
copyright: Copyright © 2025 YourName

directories:
  output: dist                        # 产物目录
  
files:
  - out/**/*                          # electron-vite 编译产物
  - resources/**/*                    # 额外资源(图标等)

# ───────────── Windows ─────────────
win:
  icon: resources/icon.ico            # 256×256 .ico
  target:
    - target: nsis                    # 安装向导(推荐)
      arch: [x64, arm64]
    - target: portable                # 免安装绿色版(可选)

nsis:
  oneClick: false                     # false = 有安装向导
  allowToChangeInstallationDirectory: true
  createDesktopShortcut: true
  createStartMenuShortcut: true

# ───────────── macOS ─────────────
mac:
  icon: resources/icon.icns           # .icns 格式
  category: public.app-category.productivity
  target:
    - target: dmg
      arch: [x64, arm64]             # 分别打,或用 universal(更大)

dmg:
  contents:
    - x: 130
      y: 220
    - x: 410
      y: 220
      type: link
      path: /Applications

# ───────────── Linux ─────────────
linux:
  icon: resources/icon.png            # 512×512 PNG
  category: Utility
  target:
    - target: AppImage               # 通用,推荐
    - target: deb                    # Debian/Ubuntu
    - target: rpm                    # Fedora/CentOS(可选)

8.2 图标准备

你需要三种格式的图标:

平台 格式 尺寸
Windows .ico 至少包含 16/32/48/64/128/256 px
macOS .icns 包含从 16 到 1024 的多尺寸
Linux .png 512×512

一键生成工具:把 1024×1024 PNG 放进去,用 electron-icon-builder:

bash 复制代码
pnpm add -D electron-icon-builder
npx electron-icon-builder --input=icon.png --output=resources/

8.3 打包命令

bash 复制代码
# 编译(不打包,检查构建是否通过)
pnpm build

# 打当前平台的包
pnpm build:win       # 出 .exe(NSIS 安装包)
pnpm build:mac       # 出 .dmg
pnpm build:linux     # 出 .AppImage + .deb

跨平台打包说明:electron-builder 尽力支持,但不是所有情况都靠谱:

  • Windows 包:建议在 Windows 机器打
  • macOS 包:必须在 macOS 打(需要真正的 macOS 工具链和证书)
  • Linux 包:Linux 或 macOS 都能打,Windows 可能有问题

8.4 产物位置

bash 复制代码
dist/
├── win-unpacked/                  # Windows 解压版(调试用)
├── My App-1.0.0-setup.exe         # Windows NSIS 安装包
├── My App-1.0.0.dmg               # macOS 磁盘镜像
├── My App-1.0.0.AppImage          # Linux 通用包
└── My App_1.0.0_amd64.deb         # Linux Debian 包

8.5 代码签名(正式发布必做)

Windows:

未签名的 exe 在用户电脑上会触发 SmartScreen 警告("Windows 已保护你的电脑")。

  1. 购买代码签名证书(DigiCert / Sectigo,EV 证书约 $200/年)

  2. 在 electron-builder.yml 中配置:

    yaml 复制代码
    win:
      certificateFile: my-cert.pfx
      certificatePassword: ${env.CERT_PASSWORD}
  3. 或用 Azure Trusted Signing(微软新方案,更便宜)。

macOS:

未签名/未公证的 app 在 macOS 上会被 Gatekeeper 拦截("已损坏,无法打开")。

yaml 复制代码
# electron-builder.yml
mac:
  identity: "Developer ID Application: Your Name (TEAM_ID)"
  hardenedRuntime: true
  entitlements: build/entitlements.mac.plist
  entitlementsInherit: build/entitlements.mac.plist
  gatekeeperAssess: false
  notarize:
    teamId: YOUR_TEAM_ID

需要 APPLE_ID、APPLE_APP_SPECIFIC_PASSWORD、APPLE_TEAM_ID 环境变量。

开发阶段临时绕过(仅自用):

bash 复制代码
# macOS 让用户跑这个
xattr -cr /Applications/MyApp.app

9. package.json 脚本参考

json 复制代码
{
  "scripts": {
    "dev": "electron-vite dev",
    "build": "electron-vite build",
    "build:win": "electron-vite build && electron-builder --win",
    "build:mac": "electron-vite build && electron-builder --mac",
    "build:linux": "electron-vite build && electron-builder --linux",
    "preview": "electron-vite preview",
    "typecheck": "tsc --noEmit"
  }
}

10. 常见问题排查

打包后白屏

根本原因:生产环境路径不对。

检查主进程里加载文件的代码:

ts 复制代码
// ❌ 错误(相对路径在打包后失效)
win.loadFile('dist/renderer/index.html')

// ✅ 正确
win.loadFile(path.join(__dirname, '../renderer/index.html'))

打开 DevTools 看 Console 报错:

ts 复制代码
win.webContents.openDevTools()

pnpm install 卡在下载 Electron

国内需要镜像,在项目根目录的 .npmrc 加:

ini 复制代码
ELECTRON_MIRROR=https://npmmirror.com/mirrors/electron/

或者:

bash 复制代码
export ELECTRON_MIRROR=https://npmmirror.com/mirrors/electron/
pnpm install

原生模块编译失败

症状:Error: The module was compiled against a different version of Node。

bash 复制代码
pnpm add -D @electron/rebuild
npx electron-rebuild

electron-builder 打包时会自动重新编译,开发时手动跑一遍就好。

ipcMain.handle 没收到调用

检查顺序:

  1. preload 里是否 exposeInMainWorld 了这个方法
  2. 主进程的 ipcMain.handle 是否在 app.whenReady() 后注册
  3. 渲染进程调的名字是否和主进程注册的名字完全一致(字符串)

macOS 打包报 "code sign" 错误

开发期不想处理签名:

yaml 复制代码
# electron-builder.yml
mac:
  identity: null

或设置环境变量跳过:

bash 复制代码
CSC_IDENTITY_AUTO_DISCOVERY=false pnpm build:mac

菜单在 macOS 显示不对

macOS 的第一个菜单永远是应用名(系统行为),你设的 File 会变成第二个。macOS 需要特殊处理:

ts 复制代码
if (process.platform === 'darwin') {
  menuTemplate.unshift({ label: app.name, role: 'appMenu' })
}

11. 安全检查清单

在提交代码前过一遍:

  • nodeIntegration: false(默认已是,不要改)
  • contextIsolation: true(默认已是,不要改)
  • preload 只暴露最小必要 API
  • ipcMain.handle 内校验所有来自渲染进程的路径参数
  • 外部链接用 shell.openExternal() 而不是 loadURL()
  • 不在渲染进程里 require('electron')

12. 自动更新

生产环境必须有自动更新,否则 bug 修复无法推到用户。Electron 用 electron-updater(electron-builder 附带)。

12.1 安装

bash 复制代码
pnpm add electron-updater

12.2 更新服务端

electron-updater 支持多种更新源:

方案 适合场景
GitHub Releases 开源或免费项目,零成本
S3 / OSS / MinIO 自建,完全控制
electron-release-server 自建服务器,支持渠道管理

以 GitHub Releases 为例(最简单),electron-builder.yml 加:

yaml 复制代码
publish:
  provider: github
  owner: your-github-username
  repo: your-repo-name

12.3 主进程更新代码

ts 复制代码
// src/main/updater.ts
import { autoUpdater } from 'electron-updater'
import { BrowserWindow, ipcMain } from 'electron'
import log from 'electron-log'

export function setupAutoUpdater(win: BrowserWindow) {
  // 生产环境才启用
  if (process.env.NODE_ENV === 'development') return

  autoUpdater.logger = log
  autoUpdater.autoDownload = false    // 不自动下载,让用户决定

  autoUpdater.on('update-available', (info) => {
    win.webContents.send('update:available', info)
  })

  autoUpdater.on('update-not-available', () => {
    win.webContents.send('update:not-available')
  })

  autoUpdater.on('download-progress', (progress) => {
    win.webContents.send('update:progress', progress)
  })

  autoUpdater.on('update-downloaded', () => {
    win.webContents.send('update:downloaded')
  })

  autoUpdater.on('error', (err) => {
    win.webContents.send('update:error', err.message)
    log.error('Updater error:', err)
  })

  // IPC:渲染进程触发检查 / 下载 / 安装
  ipcMain.handle('updater:check', () => autoUpdater.checkForUpdates())
  ipcMain.handle('updater:download', () => autoUpdater.downloadUpdate())
  ipcMain.handle('updater:install', () => autoUpdater.quitAndInstall())

  // 启动后 10 秒检查更新(不要立刻检查,影响启动体验)
  setTimeout(() => autoUpdater.checkForUpdates(), 10_000)
}

在 createWindow 完成后调用:

ts 复制代码
app.whenReady().then(() => {
  const win = createWindow()
  setupAutoUpdater(win)
})

12.4 Preload 暴露

ts 复制代码
// src/preload/index.ts 追加
contextBridge.exposeInMainWorld('updater', {
  check: () => ipcRenderer.invoke('updater:check'),
  download: () => ipcRenderer.invoke('updater:download'),
  install: () => ipcRenderer.invoke('updater:install'),
  onAvailable: (cb: (info: unknown) => void) =>
    ipcRenderer.on('update:available', (_e, info) => cb(info)),
  onProgress: (cb: (p: unknown) => void) =>
    ipcRenderer.on('update:progress', (_e, p) => cb(p)),
  onDownloaded: (cb: () => void) =>
    ipcRenderer.on('update:downloaded', cb),
})

12.5 发布流程

bash 复制代码
# 打包并发布到 GitHub Releases(需要 GH_TOKEN 环境变量)
GH_TOKEN=your_token pnpm build:win

electron-builder 会上传安装包 + latest.yml(更新元数据文件)到 GitHub Releases,客户端通过读这个文件判断是否有新版本。


13. 多窗口管理

生产应用常有多种窗口:主窗口、设置窗口、关于窗口、全屏遮罩等。

13.1 窗口管理器

ts 复制代码
// src/main/window-manager.ts
import { BrowserWindow, screen } from 'electron'
import path from 'node:path'

type WindowName = 'main' | 'settings' | 'about'

class WindowManager {
  private windows = new Map<WindowName, BrowserWindow>()

  private baseOpts = {
    webPreferences: {
      preload: path.join(__dirname, '../preload/index.js'),
      contextIsolation: true,
      nodeIntegration: false,
    },
  }

  create(name: WindowName, opts: Electron.BrowserWindowConstructorOptions = {}) {
    // 已存在则聚焦,不重复创建
    const existing = this.windows.get(name)
    if (existing && !existing.isDestroyed()) {
      existing.focus()
      return existing
    }

    const win = new BrowserWindow({ ...this.baseOpts, ...opts })

    win.on('closed', () => this.windows.delete(name))
    this.windows.set(name, win)

    const url = process.env.NODE_ENV === 'development'
      ? `http://localhost:5173/#${name}`
      : `file://${path.join(__dirname, '../renderer/index.html')}#${name}`

    win.loadURL(url)
    return win
  }

  get(name: WindowName) {
    return this.windows.get(name)
  }

  closeAll() {
    this.windows.forEach((win) => { if (!win.isDestroyed()) win.close() })
  }
}

export const wm = new WindowManager()

使用:

ts 复制代码
// 主进程
import { wm } from './window-manager'

app.whenReady().then(() => {
  wm.create('main', { width: 1200, height: 800 })
})

ipcMain.handle('window:openSettings', () => {
  wm.create('settings', { width: 600, height: 500, parent: wm.get('main') })
})

13.2 窗口状态持久化

关闭后记住窗口位置和大小,下次打开还原:

bash 复制代码
pnpm add electron-window-state
ts 复制代码
import windowStateKeeper from 'electron-window-state'

function createMainWindow() {
  const state = windowStateKeeper({
    defaultWidth: 1200,
    defaultHeight: 800,
  })

  const win = new BrowserWindow({
    x: state.x,
    y: state.y,
    width: state.width,
    height: state.height,
    webPreferences: { /* ... */ },
  })

  state.manage(win)   // 自动保存 resize/move 事件
  return win
}

14. 环境变量与配置管理

14.1 开发 vs 生产区分

electron-vite 已处理好,但要注意几个常见需求:

ts 复制代码
// 判断环境
const isDev = process.env.NODE_ENV === 'development'
const isProd = !isDev

// app.isPackaged 是更可靠的"是否打包了"判断
const isPackaged = app.isPackaged

14.2 环境变量文件

项目根目录创建(不要提交到 git):

bash 复制代码
.env                  # 所有环境共用
.env.development      # 仅开发
.env.production       # 仅生产
.env.local            # 本地覆盖(最高优先级)
bash 复制代码
# .env.development
VITE_API_URL=http://localhost:3000
VITE_LOG_LEVEL=debug

# .env.production
VITE_API_URL=https://api.yourapp.com
VITE_LOG_LEVEL=warn

渲染进程里用 import.meta.env.VITE_* 访问(只有 VITE_ 前缀的变量会暴露给渲染进程)。

主进程里用 process.env.*,但 electron-vite 不会自动注入 .env 到主进程------需要用 dotenv:

ts 复制代码
// src/main/index.ts 顶部
import { config } from 'dotenv'
config({ path: `.env.${process.env.NODE_ENV}` })

14.3 生产配置文件(用户数据目录)

用户级别的配置(用户偏好、账号信息等)存在 userData 目录:

ts 复制代码
// src/main/config.ts
import { app } from 'electron'
import { readFile, writeFile } from 'node:fs/promises'
import path from 'node:path'

interface AppConfig {
  theme: 'light' | 'dark' | 'system'
  language: string
  windowBounds?: { x: number; y: number; width: number; height: number }
}

const CONFIG_PATH = path.join(app.getPath('userData'), 'config.json')
const DEFAULTS: AppConfig = { theme: 'system', language: 'zh-CN' }

export async function loadConfig(): Promise<AppConfig> {
  try {
    const raw = await readFile(CONFIG_PATH, 'utf-8')
    return { ...DEFAULTS, ...JSON.parse(raw) }
  } catch {
    return DEFAULTS
  }
}

export async function saveConfig(config: Partial<AppConfig>) {
  const current = await loadConfig()
  await writeFile(CONFIG_PATH, JSON.stringify({ ...current, ...config }, null, 2))
}

15. 日志与错误监控

15.1 electron-log(本地日志)

bash 复制代码
pnpm add electron-log
ts 复制代码
// src/main/logger.ts
import log from 'electron-log'
import path from 'node:path'
import { app } from 'electron'

// 配置日志文件路径(默认已在 userData 下)
log.transports.file.resolvePathFn = () =>
  path.join(app.getPath('userData'), 'logs', 'main.log')

log.transports.file.maxSize = 10 * 1024 * 1024  // 10 MB 后轮转
log.transports.console.level = process.env.NODE_ENV === 'development' ? 'debug' : 'warn'
log.transports.file.level = 'info'

export default log

渲染进程里:

ts 复制代码
import log from 'electron-log/renderer'

log.info('User clicked save')
log.error('Failed to load config', error)

15.2 Sentry(生产错误上报)

bash 复制代码
pnpm add @sentry/electron
ts 复制代码
// src/main/index.ts
import * as Sentry from '@sentry/electron/main'

Sentry.init({
  dsn: process.env.SENTRY_DSN,
  environment: app.isPackaged ? 'production' : 'development',
  release: app.getVersion(),
  // 不要在开发时上报
  enabled: app.isPackaged,
})
ts 复制代码
// src/renderer/src/main.tsx
import * as Sentry from '@sentry/electron/renderer'

Sentry.init({
  dsn: import.meta.env.VITE_SENTRY_DSN,
})

16. 性能优化

16.1 懒加载渲染进程内容

tsx 复制代码
// 路由级别懒加载
import { lazy, Suspense } from 'react'

const HeavyFeature = lazy(() => import('./HeavyFeature'))

function App() {
  return (
    <Suspense fallback={<div>Loading...</div>}>
      <HeavyFeature />
    </Suspense>
  )
}

16.2 主进程 CPU 密集任务:用 Worker

主进程里跑重计算会阻塞 IPC 响应,用 worker_threads:

ts 复制代码
// src/main/workers/hash-worker.ts
import { parentPort, workerData } from 'node:worker_threads'
import { createHash } from 'node:crypto'

const hash = createHash('sha256').update(workerData.content).digest('hex')
parentPort?.postMessage(hash)
ts 复制代码
// src/main/index.ts
import { Worker } from 'node:worker_threads'
import path from 'node:path'

ipcMain.handle('file:hash', (_e, content: string) => {
  return new Promise((resolve, reject) => {
    const worker = new Worker(
      path.join(__dirname, 'workers/hash-worker.js'),
      { workerData: { content } }
    )
    worker.on('message', resolve)
    worker.on('error', reject)
  })
})

16.3 渲染进程内存泄漏常见原因

ts 复制代码
// ❌ 监听器未清理
useEffect(() => {
  window.electronAPI.onThemeChange(handleTheme)
  // 忘记返回清理函数
}, [])

// ✅ 正确
useEffect(() => {
  const unlisten = window.electronAPI.onThemeChange(handleTheme)
  return () => unlisten()   // 组件卸载时清理
}, [])

preload 里监听器也要提供清理方法:

ts 复制代码
contextBridge.exposeInMainWorld('electronAPI', {
  onThemeChange: (cb: (theme: string) => void) => {
    const handler = (_e: unknown, theme: string) => cb(theme)
    ipcRenderer.on('theme:changed', handler)
    // 返回清理函数
    return () => ipcRenderer.removeListener('theme:changed', handler)
  },
})

16.4 冷启动优化

ts 复制代码
// 主窗口先隐藏,ready-to-show 后再显示(避免白屏闪烁)
const win = new BrowserWindow({
  show: false,
  backgroundColor: '#ffffff',   // 设背景色避免黑屏
  webPreferences: { /* ... */ },
})

win.once('ready-to-show', () => win.show())

17. CI/CD(GitHub Actions)

17.1 多平台自动构建并发布

yaml 复制代码
# .github/workflows/release.yml
name: Release

on:
  push:
    tags:
      - 'v*'    # 推 v1.0.0 这样的 tag 触发

permissions:
  contents: write

jobs:
  build:
    strategy:
      matrix:
        include:
          - os: windows-latest
            script: build:win
          - os: macos-latest
            script: build:mac
          - os: ubuntu-latest
            script: build:linux

    runs-on: ${{ matrix.os }}

    steps:
      - uses: actions/checkout@v4

      - uses: pnpm/action-setup@v4
        with:
          version: 9

      - uses: actions/setup-node@v4
        with:
          node-version: 20
          cache: 'pnpm'

      - name: Install dependencies
        run: pnpm install --frozen-lockfile

      - name: Build & publish
        run: pnpm ${{ matrix.script }}
        env:
          GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
          # macOS 签名
          CSC_LINK: ${{ secrets.MAC_CERT_P12_BASE64 }}
          CSC_KEY_PASSWORD: ${{ secrets.MAC_CERT_PASSWORD }}
          APPLE_ID: ${{ secrets.APPLE_ID }}
          APPLE_APP_SPECIFIC_PASSWORD: ${{ secrets.APPLE_APP_SPECIFIC_PASSWORD }}
          APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
          # Windows 签名(可选)
          WIN_CSC_LINK: ${{ secrets.WIN_CERT_P12_BASE64 }}
          WIN_CSC_KEY_PASSWORD: ${{ secrets.WIN_CERT_PASSWORD }}

触发方式:

bash 复制代码
git tag v1.0.1
git push origin v1.0.1

Actions 会在三个平台并行构建,完成后自动创建 GitHub Release 并上传安装包。

17.2 macOS 证书在 CI 里的处理

macOS 签名证书不能直接放进仓库,通过 Secrets 传:

bash 复制代码
# 本地:把证书 base64 编码
base64 -i certificate.p12 | pbcopy   # macOS

把输出粘贴到 Settings → Secrets → MAC_CERT_P12_BASE64。


18. 完整安全检查清单(生产版)

在发布前过一遍:

进程隔离

  • nodeIntegration: false
  • contextIsolation: true
  • sandbox: true(可选但推荐,限制主进程权限)
  • preload 只暴露最小必要 API

输入校验

  • 所有从渲染进程来的路径参数都做路径穿越检查
  • 所有 IPC handler 都做参数类型校验(用 zod 或手动)
  • 外部 URL 只通过 shell.openExternal(),不 loadURL()

内容安全策略

  • 设置 CSP header,禁止 unsafe-eval 和 unsafe-inline:

    ts 复制代码
    win.webContents.session.webRequest.onHeadersReceived((_details, callback) => {
      callback({
        responseHeaders: {
          'Content-Security-Policy': [
            "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'"
          ],
        },
      })
    })

依赖安全

  • 定期跑 pnpm audit,修复 high/critical 漏洞
  • 不在 dependencies 里放只有开发需要的包(增大安装包)

发布安全

  • 代码签名(Windows + macOS)
  • macOS 公证
  • 不在日志里输出敏感信息(Token、密码)

19. 参考资源

相关推荐
卸任21 小时前
Vibe coding工具使用心得
前端·ai编程
brien91 天前
为什么模型标称 1M,Google Antigravity 聊到 256K 却突然失忆?我们给它做了个原生行内监控圆环
前端
修炼的dance1 天前
录屏关键帧间隔多少合适?六档实测,2秒到10秒小了75.6%
前端
Lank_M1 天前
关键帧从1秒拉到10秒,录屏小87.3%,摄像头小10.8%
前端
补码缩写1 天前
用 ffprobe 拆录屏,八成字节在 29 个关键帧上
前端
百思不得小李1 天前
一个复杂的动态表单是如何实现的
前端·设计模式
陈随易1 天前
Cmder 停更 17 个月,2.0 支持 Windows Terminal
前端·后端·程序员
风骏时光牛马1 天前
前沿视界
前端
kyriewen1 天前
我扫了 814 个依赖,97 个还敢要 Node 10
前端·javascript·程序员
陈随易1 天前
3000 万人在用的翻译插件,重写一遍就是一门生意
前端·后端·程序员