Electron 桌面應用開發完全指南:從環境搭建到生產發布
為什麼選擇 Electron
跨平台桌面應用開發一直是個讓人頭痛的問題。寫 Windows 用 C# 和 WPF,寫 macOS 用 Swift 和 AppKit,寫 Linux 用 GTK 或 Qt——三套程式碼,三份維護成本。Electron 的出現改變了這個局面:一套 HTML/CSS/JavaScript,打包後同時跑在 Windows、macOS 和 Linux 上。
但凡用過 VS Code、Figma、Slack、Discord 的開發者,其實每天都在用 Electron 應用。VSCode 的啟動速度、Figma 的渲染效能,證明了 Electron 如果架構得當,體驗完全可以媲美原生。
我幫團隊遷移過一個內部工具——從 C# WinForms 改到 Electron。後續兩個季度,新功能的迭代速度提升了將近三倍。原因很簡單:招前端比招 C# 桌面開發容易得多,組件生態也豐富得多。
這篇文章是我踩坑後的完整總結。我不會羅列 API 文件——那些你隨時能查到——我關注的是實際項目中真正會遇到的問題和對應的解決方案。
項目初始化:別再抄官方的 quick-start 了
官方 electron-quick-start 範例用裸 JavaScript,沒有構建工具,沒有模組化,沒有熱更新。真實項目這樣起步,兩個月後你會在屎山裡改 bug。
推薦用 electron-vite 或者手動搭建 Vite + Electron:
npm create @quick-start/electron@latest my-app -- --template vue-ts
或者手搭:
mkdir my-electron-app && cd my-electron-app
npm init -y
npm i -D electron electron-builder vite @vitejs/plugin-vue typescript
npm i vue
建議的專案目錄結構:
my-electron-app/
├── electron/ # 主進程程式碼
│ ├── main.ts # 入口,建立視窗
│ ├── preload.ts # 預載入腳本(安全橋接)
│ └── ipc/ # IPC 通道管理
├── src/ # 渲染進程(前端程式碼)
│ ├── App.vue
│ ├── main.ts
│ └── components/
├── resources/ # 應用圖示等靜態資源
├── electron-builder.yml # 打包配置
├── vite.config.ts
└── package.json
這裡最關鍵的設計決策是:主進程和渲染進程完全分離。主進程 (electron/) 只做系統級操作,渲染進程 (src/) 是標準的前端項目。
程序模型:搞清楚誰負責什麼
Electron 有兩個核心程序,不把這個概念吃透,後續的安全問題和效能問題會讓你反覆返工。
| 程序 | 執行環境 | 能做什麼 | 不能做什麼 |
|---|---|---|---|
| 主進程 (Main) | Node.js | 建立視窗、讀寫檔案、呼叫系統 API、管理應用生命週期 | 不能直接操作 DOM |
| 渲染進程 (Renderer) | Chromium | 渲染 HTML/CSS、回應使用者互動、操作 DOM | 預設不能訪問 Node.js API |
兩者通過 IPC(程序間通訊) 橋接。主進程暴露有限的 API 給渲染進程,渲染進程通過 ipcRenderer 呼叫。
一個典型的通訊流程:
使用者點選「儲存檔案」
→ 渲染進程: ipcRenderer.invoke('save-file', fileData)
→ 主進程: ipcMain.handle('save-file', async (event, data) => {
dialog.showSaveDialog() → fs.writeFileSync()
})
→ 返回結果給渲染進程
Preload 腳本——應用的安全門
// electron/preload.ts
import { contextBridge, ipcRenderer } from 'electron';
contextBridge.exposeInMainWorld('electronAPI', {
saveFile: (content: string, defaultName?: string) =>
ipcRenderer.invoke('save-file', content, defaultName),
openFile: () =>
ipcRenderer.invoke('open-file'),
getAppVersion: () => ipcRenderer.invoke('get-app-version'),
getPlatform: () => process.platform,
minimizeWindow: () => ipcRenderer.send('window-minimize'),
maximizeWindow: () => ipcRenderer.send('window-maximize'),
closeWindow: () => ipcRenderer.send('window-close'),
});
渲染進程中這樣呼叫:
// src/components/SaveButton.vue
const handleSave = async () => {
const result = await window.electronAPI.saveFile(
editorContent.value,
'untitled.txt'
);
if (result.success) {
showToast(`已儲存到 ${result.filePath}`);
}
};
contextBridge 是關鍵——它限制了渲染進程可呼叫的 API 範圍。永遠不要直接暴露 ipcRenderer 給渲染進程,那樣等於把 Node.js 完全開放給了網頁內容。
安全性:七個必須遵守的規則
Electron 的安全模型經歷了多次迭代,舊教學裡的做法很多已經不安全了。截至 2026 年,以下是硬性要求:
| # | 規則 | 配置 | 原因 |
|---|---|---|---|
| 1 | contextIsolation: true |
webPreferences | 隔離渲染進程的 JS 上下文和 preload |
| 2 | nodeIntegration: false |
webPreferences | 禁止渲染進程直接 require Node 模組 |
| 3 | sandbox: true |
webPreferences | 作業系統級沙箱,限制程序權限 |
| 4 | 使用 contextBridge |
preload.ts | 白名單式暴露 API,而非直接掛載 |
| 5 | 驗證 IPC 訊息來源 | ipcMain.handle | event.senderFrame 校驗呼叫方 URL |
| 6 | 禁止遠端內容 | webPreferences | 不載入遠端 URL,除非有 CSP 策略 |
| 7 | 簽名和公證 | electron-builder | macOS 必須公證,Windows 建議簽名 |
視窗管理:不僅僅是 new BrowserWindow
無邊框視窗與自訂標題列
大多數現代桌面應用都拋棄了系統原生標題列——VS Code、Figma、Notion 都是這樣。實作方式:
const mainWindow = new BrowserWindow({
frame: false,
titleBarStyle: 'hidden',
...(process.platform === 'darwin'
? { titleBarStyle: 'hiddenInset' }
: { frame: false }),
});
然後在渲染進程中實作自訂標題列:
<!-- src/components/TitleBar.vue -->
<template>
<div class="title-bar" :class="{ 'is-mac': isMac }">
<div class="title-bar-drag" @dblclick="toggleMaximize">
<span class="app-title">{{ title }}</span>
</div>
<div class="window-controls" v-if="!isMac">
<button @click="minimize" class="control-btn minimize">─</button>
<button @click="toggleMaximize" class="control-btn maximize">□</button>
<button @click="close" class="control-btn close">✕</button>
</div>
</div>
</template>
注意 -webkit-app-region: drag ——這行 CSS 讓標題列區域支援拖曳移動視窗。按鈕區域必須用 no-drag 覆蓋。
自動更新:使用者無感升級
自動更新是桌面應用的標配。electron-updater(基於 electron-builder)是最成熟的方案:
// electron/updater.ts
import { autoUpdater } from 'electron-updater';
export function setupAutoUpdater(mainWindow: BrowserWindow) {
autoUpdater.setFeedURL({
provider: 'generic',
url: 'https://releases.your-app.com/updates/',
});
setInterval(() => {
autoUpdater.checkForUpdates();
}, 4 * 60 * 60 * 1000);
autoUpdater.on('update-downloaded', () => {
dialog.showMessageBox(mainWindow, {
type: 'info',
title: '更新已就緒',
message: '新版本已下載,點選確認立即重啟安裝',
}).then(() => {
autoUpdater.quitAndInstall();
});
});
setTimeout(() => autoUpdater.checkForUpdates(), 5000);
}
打包與分發
electron-builder.yml 配置範例:
appId: com.yourcompany.yourapp
productName: YourApp
directories:
output: release
files:
- dist/**/*
- electron/**/*.js
- package.json
win:
target:
- target: nsis
arch: [x64, arm64]
icon: resources/icon.ico
mac:
target:
- target: dmg
arch: [x64, arm64]
icon: resources/icon.icns
category: public.app-category.developer-tools
hardenedRuntime: true
notarize:
teamId: YOUR_TEAM_ID
linux:
target:
- target: AppImage
arch: [x64]
- target: deb
arch: [x64]
category: Development
nsis:
oneClick: false
allowToChangeInstallationDirectory: true
效能最佳化:別讓你的應用變成 Chrome
Electron 被詬病最多的是記憶體佔用。一個空 Electron 自帶 50-80MB,加上實際頁面很容易上 200MB。
延遲載入非首屏視窗
let settingsWindow: BrowserWindow | null = null;
ipcMain.handle('open-settings', async () => {
if (settingsWindow && !settingsWindow.isDestroyed()) {
settingsWindow.focus();
return;
}
settingsWindow = new BrowserWindow({ /* ... */ });
settingsWindow.loadURL('...');
settingsWindow.on('closed', () => { settingsWindow = null; });
});
記憶體洩漏排查清單
| 檢查項 | 工具 | 指標 |
|---|---|---|
| 關閉視窗後程序未退出 | 任務管理器 / Activity Monitor | 殘留 electron 程序 |
| webContents 未銷毀 | webContents.getAllWebContents() |
數量持續增長 |
| IPC 監聽未清理 | ipcMain.eventNames() |
removeHandler 遺漏 |
| 大物件引用未釋放 | Chrome DevTools Memory 面板 | 堆快照對比 |
常見踩坑記錄
坑 1:macOS 公證失敗
"notarization failed: The binary is not signed"
解決:確認 hardenedRuntime: true,entitlements 檔案正確,且使用 Apple Developer ID 憑證。
坑 2:asar 打包後 native 模組載入失敗
# electron-builder.yml
asarUnpack:
- "node_modules/sharp/**"
- "node_modules/better-sqlite3/**"
- "native/**/*.node"
坑 3:啟動白屏時間過長
mainWindow.once('ready-to-show', () => { mainWindow.show(); });
相關工具
- Base64 編碼解碼 — 處理內嵌資源與檔案傳輸
- JSON 格式化 — 除錯 IPC 訊息與配置檔案
- 圖片壓縮 — 最佳化應用內圖片資源的體積
本站提供瀏覽器本地工具,免註冊即可試用 →