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 对象
  • 职责:把主进程能力安全地暴露给渲染进程(白名单机制)
  • 关键 APIcontextBridge.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_IDAPPLE_APP_SPECIFIC_PASSWORDAPPLE_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-evalunsafe-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. 参考资源

相关推荐
小则又沐风a1 小时前
深入了解进程概念 第二章
java·linux·服务器·前端
亲亲小宝宝鸭1 小时前
微前端方案探索:qiankun
前端·微服务
渐儿1 小时前
跨端框架实操开发文档:Electron / Tauri / React Native
前端
ZC跨境爬虫1 小时前
跟着 MDN 学 HTML day_60:(表单与按钮技能测试实战)
服务器·前端·javascript·数据库·ui·html
lihaozecq1 小时前
做 Agent SDK 必须支持的插件能力:8 个钩子搞定横切关注点
前端·agent·ai编程
秦歌6661 小时前
Agent Skills详解
服务器·前端·数据库
ljt27249606611 小时前
Vue笔记(四)--组件基础
前端·vue.js·笔记
哈撒Ki1 小时前
快速入门WebSocket
前端·websocket
张元清1 小时前
React 里不用 setTimeout 的计时器写法:useTimeout、useInterval、useCountDown 和 useRafFn
前端·javascript·面试