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(); });

相關工具

本站提供瀏覽器本地工具,免註冊即可試用 →

#Electron#桌面应用#跨平台#Node.js#教程