{T}

Electron 跨平台兼容性权威指南

1. 引言

Electron 以其 "一次编写,处处运行" 的承诺,极大地简化了桌面应用的开发。然而,尽管它在抽象底层差异方面做得非常出色,Windows、macOS 和 Linux 三大操作系统之间固有的平台差异依然是开发者必须面对的挑战。

1.1 文档目标

本指南旨在:

  • 全面梳理 Electron 开发中常见的跨平台兼容性问题
  • 提供经过验证的最佳实践和解决方案
  • 建立清晰的平台适配开发流程
  • 帮助开发者规避常见陷阱

1.2 适用读者

  • Electron 初学者:了解基础的平台差异概念
  • 中级开发者:掌握跨平台适配技巧
  • 高级开发者:深入理解底层机制和性能优化

2. 系统架构与核心概念

2.1 多进程架构概述

理解 Electron 的多进程架构是处理平台差异的基石。

plaintext
┌─────────────────────────────────────────────────────────────┐
│                     Electron 应用架构                        │
├─────────────────────────────────────────────────────────────┤
│                                                              │
│  ┌──────────────────────┐      IPC 通信     ┌─────────────┐│
│  │      主进程          │◄───────────────►│  渲染进程    ││
│  │  (Main Process)      │                  │  (Renderer) ││
│  │                      │                  │             ││
│  │  • 应用生命周期      │                  │  • UI 渲染  ││
│  │  • 原生 API 调用     │                  │  • DOM 操作 ││
│  │  • 窗口管理          │                  │  • 用户交互 ││
│  │  • 平台判断逻辑      │                  │             ││
│  └──────────────────────┘                  └─────────────┘│
│           │                                       │        │
│           ▼                                       ▼        │
│  ┌──────────────────────┐              ┌─────────────┐    │
│  │    Node.js 环境      │              │ Chromium    │    │
│  │    • 文件系统        │              │ 环境        │    │
│  │    • 系统调用        │              │ • Web API   │    │
│  │    • 原生模块        │              │ • CSS       │    │
│  └──────────────────────┘              └─────────────┘    │
│                                                              │
└─────────────────────────────────────────────────────────────┘

2.2 进程职责划分

进程类型主要职责平台相关代码位置注意事项
主进程应用生命周期管理<br>原生 API 调用<br>窗口创建与管理<br>系统托盘与菜单绝大多数平台判断和系统交互逻辑唯一实例,负责协调所有渲染进程
渲染进程UI 渲染与交互<br>DOM 操作<br>部分 UI 微调平台特定的样式调整<br>通过 IPC 请求主进程服务多实例,相互隔离

2.3 IPC 通信最佳实践

javascript
// 主进程 - 处理平台相关的文件操作
import { ipcMain } from 'electron'
import { isMac } from './platform'
 
ipcMain.handle('get-default-save-path', (event, filename) => {
  const basePath = isMac 
    ? app.getPath('documents') 
    : app.getPath('downloads')
  return path.join(basePath, filename)
})
 
// 渲染进程 - 发起请求
const savePath = await ipcRenderer.invoke('get-default-save-path', 'report.pdf')

核心原则: 将所有系统级交互和平台判断逻辑收敛在主进程,渲染进程仅负责 UI 展示和用户交互。


3. 平台识别与条件执行

3.1 平台标识符

Electron 通过 Node.js 的 process.platform 属性提供平台识别能力:

返回值操作系统备注
'win32'Windows包含 64 位 Windows
'darwin'macOS包含 Intel 和 Apple Silicon
'linux'Linux所有 Linux 发行版

3.2 平台工具模块

创建可复用的平台工具模块 src/main/platform.js:

javascript
import { platform, arch } from 'node:process'
 
// 平台检测
export const isMac = platform === 'darwin'
export const isWindows = platform === 'win32'
export const isLinux = platform === 'linux'
 
// 架构检测
export const isArm64 = arch === 'arm64'
export const isX64 = arch === 'x64'
 
// 组合检测
export const isMacArm = isMac && isArm64
export const isMacIntel = isMac && isX64
 
// 获取平台名称(用于日志和 UI 显示)
export function getPlatformName() {
  if (isMac) return 'macOS'
  if (isWindows) return 'Windows'
  if (isLinux) return 'Linux'
  return 'Unknown'
}
 
// 获取完整的平台信息
export function getPlatformInfo() {
  return {
    platform: platform,
    arch: arch,
    isMac,
    isWindows,
    isLinux,
    platformName: getPlatformName()
  }
}

3.3 渲染进程中的平台检测

渲染进程无法直接访问 process,需通过以下方式获取平台信息:

javascript
// 方法 1: 通过 preload 脚本暴露
// preload.js
import { contextBridge } from 'electron'
import { getPlatformInfo } from './platform'
 
contextBridge.exposeInMainWorld('electronPlatform', {
  ...getPlatformInfo()
})
 
// 渲染进程中使用
console.log(window.electronPlatform.platformName) // 'macOS' / 'Windows' / 'Linux'
javascript
// 方法 2: 通过 CSS 类名(推荐用于样式适配)
// main.js
import { getPlatformInfo } from './platform'
 
function createWindow() {
  const mainWindow = new BrowserWindow({
    // ...其他配置
  })
  
  mainWindow.loadFile('index.html').then(() => {
    // 注入平台类名
    const { platform } = getPlatformInfo()
    mainWindow.webContents.executeJavaScript(
      `document.body.classList.add('platform-${platform}')`
    )
  })
}
css
/* 样式适配示例 */
.titlebar {
  height: 32px;
  -webkit-app-region: drag;
}
 
.platform-darwin .titlebar {
  padding-left: 78px; /* 为交通灯按钮留出空间 */
}
 
.platform-win32 .titlebar {
  padding-right: 138px; /* 为窗口控件留出空间 */
}

4. 详细平台差异与解决方案

4.1 窗口管理

4.1.1 窗口关闭行为

差异说明:

plaintext
┌─────────────┬──────────────────────────────────┐
│   平台      │          默认行为                │
├─────────────┼──────────────────────────────────┤
│ Windows     │ 关闭所有窗口 → 应用退出          │
│ Linux       │ 关闭所有窗口 → 应用退出          │
│ macOS       │ 关闭所有窗口 → 应用继续运行      │
│             │ (Dock 栏保持活动状态)            │
└─────────────┴──────────────────────────────────┘

完整解决方案:

javascript
// main.js
import { app, BrowserWindow } from 'electron'
import { isMac } from './platform'
 
let mainWindow = null
 
function createWindow() {
  mainWindow = new BrowserWindow({
    width: 1200,
    height: 800,
    // ...其他配置
  })
  
  mainWindow.on('closed', () => {
    mainWindow = null
  })
}
 
// 窗口全部关闭时的处理
app.on('window-all-closed', () => {
  if (!isMac) {
    app.quit()
  }
})
 
// macOS 点击 Dock 图标重新创建窗口
app.on('activate', () => {
  if (BrowserWindow.getAllWindows().length === 0) {
    createWindow()
  }
})
 
// 应用即将退出时的清理工作
app.on('before-quit', () => {
  // 执行清理操作,如保存未保存的文档
  console.log('应用即将退出')
})

4.1.2 窗口控件与自定义标题栏

差异说明:

平台控件位置控件样式默认行为
macOS左侧红/黄/绿交通灯点击红色关闭窗口但不退出应用
Windows右侧关闭/最小化/最大化关闭窗口通常退出应用

自定义标题栏实现:

javascript
// main.js - 创建无边框窗口
function createWindow() {
  const mainWindow = new BrowserWindow({
    width: 1200,
    height: 800,
    frame: false,        // 无边框
    titleBarStyle: 'hidden', // macOS 隐藏标题栏但保留交通灯
    trafficLightPosition: { x: 16, y: 16 }, // macOS 交通灯位置
    // Windows 特定设置
    ...(isWindows && {
      autoHideMenuBar: true,
    })
  })
}
html
<!-- 自定义标题栏 HTML -->
<div class="titlebar">
  <div class="titlebar-drag-area">
    <span class="app-title">我的应用</span>
  </div>
  <div class="window-controls">
    <button class="control-btn minimize" onclick="minimizeWindow()">─</button>
    <button class="control-btn maximize" onclick="maximizeWindow()">□</button>
    <button class="control-btn close" onclick="closeWindow()">✕</button>
  </div>
</div>
javascript
// 窗口控制函数
const { ipcRenderer } = require('electron')
 
function minimizeWindow() {
  ipcRenderer.send('window-minimize')
}
 
function maximizeWindow() {
  ipcRenderer.send('window-maximize')
}
 
function closeWindow() {
  ipcRenderer.send('window-close')
}
javascript
// 主进程处理窗口控制
ipcMain.on('window-minimize', (event) => {
  BrowserWindow.fromWebContents(event.sender)?.minimize()
})
 
ipcMain.on('window-maximize', (event) => {
  const win = BrowserWindow.fromWebContents(event.sender)
  if (win) {
    win.isMaximized() ? win.unmaximize() : win.maximize()
  }
})
 
ipcMain.on('window-close', (event) => {
  BrowserWindow.fromWebContents(event.sender)?.close()
})

4.1.3 窗口状态管理

javascript
// 保存和恢复窗口状态
import Store from 'electron-store'
 
const store = new Store()
 
function createWindow() {
  // 从存储中恢复窗口状态
  const savedBounds = store.get('windowBounds')
  
  const mainWindow = new BrowserWindow({
    width: savedBounds?.width || 1200,
    height: savedBounds?.height || 800,
    x: savedBounds?.x,
    y: savedBounds?.y,
    // ...其他配置
  })
  
  // 窗口移动或调整大小时保存状态
  mainWindow.on('moved', saveWindowState)
  mainWindow.on('resized', saveWindowState)
  
  // 如果之前是最大化状态,恢复
  if (store.get('windowMaximized')) {
    mainWindow.maximize()
  }
}
 
function saveWindowState() {
  const mainWindow = BrowserWindow.getFocusedWindow()
  if (mainWindow && !mainWindow.isMaximized()) {
    store.set('windowBounds', mainWindow.getBounds())
  }
  store.set('windowMaximized', mainWindow?.isMaximized() || false)
}

4.2 菜单和快捷键

4.2.1 应用菜单

平台差异:

plaintext
┌─────────────┬──────────────────────────────────┐
│   平台      │         菜单位置与行为           │
├─────────────┼──────────────────────────────────┤
│ macOS       │ 全局菜单栏,固定在屏幕顶部        │
│             │ 第一个菜单为应用菜单             │
│ Windows     │ 菜单栏在窗口标题栏下方           │
│ Linux       │ 取决于桌面环境                   │
└─────────────┴──────────────────────────────────┘

动态菜单构建:

javascript
import { app, Menu } from 'electron'
import { isMac } from './platform'
 
function createAppMenu() {
  const template = [
    // macOS 专属的应用菜单
    ...(isMac
      ? [
          {
            label: app.name,
            submenu: [
              { role: 'about' },
              { type: 'separator' },
              { role: 'services' },
              { type: 'separator' },
              { role: 'hide' },
              { role: 'hideOthers' },
              { role: 'unhide' },
              { type: 'separator' },
              { role: 'quit' }
            ]
          }
        ]
      : []),
    
    // 文件菜单
    {
      label: '文件',
      submenu: [
        {
          label: '新建',
          accelerator: 'CmdOrCtrl+N',
          click: () => createNewDocument()
        },
        {
          label: '打开...',
          accelerator: 'CmdOrCtrl+O',
          click: () => openFile()
        },
        { type: 'separator' },
        {
          label: '保存',
          accelerator: 'CmdOrCtrl+S',
          click: () => saveFile()
        },
        { type: 'separator' },
        isMac ? { role: 'close' } : { role: 'quit' }
      ]
    },
    
    // 编辑菜单
    {
      label: '编辑',
      submenu: [
        { role: 'undo', label: '撤销' },
        { role: 'redo', label: '重做' },
        { type: 'separator' },
        { role: 'cut', label: '剪切' },
        { role: 'copy', label: '复制' },
        { role: 'paste', label: '粘贴' },
        { role: 'selectAll', label: '全选' }
      ]
    },
    
    // 视图菜单
    {
      label: '视图',
      submenu: [
        { role: 'reload', label: '重新加载' },
        { role: 'forceReload', label: '强制重新加载' },
        { role: 'toggleDevTools', label: '开发者工具' },
        { type: 'separator' },
        { role: 'resetZoom', label: '实际大小' },
        { role: 'zoomIn', label: '放大' },
        { role: 'zoomOut', label: '缩小' },
        { type: 'separator' },
        { role: 'togglefullscreen', label: '全屏' }
      ]
    },
    
    // 窗口菜单
    {
      label: '窗口',
      submenu: [
        { role: 'minimize', label: '最小化' },
        { role: 'zoom', label: '缩放' },
        ...(isMac
          ? [
              { type: 'separator' },
              { role: 'front', label: '前置全部窗口' }
            ]
          : [
              { role: 'close', label: '关闭' }
            ])
      ]
    },
    
    // 帮助菜单
    {
      role: 'help',
      label: '帮助',
      submenu: [
        {
          label: '学习更多',
          click: async () => {
            const { shell } = require('electron')
            await shell.openExternal('https://electronjs.org')
          }
        },
        {
          label: '关于',
          click: () => showAboutDialog()
        }
      ]
    }
  ]
  
  const menu = Menu.buildFromTemplate(template)
  Menu.setApplicationMenu(menu)
}

4.2.2 快捷键修饰符

平台映射关系:

Electron 修饰符macOSWindows/Linux
CommandOrControl / CmdOrCtrlCommand (⌘)Control (Ctrl)
Option / AltOption (⌥)Alt
ShiftShift (⇧)Shift
Super / MetaCommand (⌘)Windows 键 / Super

最佳实践示例:

javascript
import { globalShortcut } from 'electron'
 
function registerGlobalShortcuts() {
  // 跨平台快捷键
  globalShortcut.register('CommandOrControl+Shift+V', () => {
    console.log('跨平台粘贴快捷键被触发')
  })
  
  // macOS 专属快捷键
  if (isMac) {
    globalShortcut.register('Command+Option+I', () => {
      console.log('macOS 专属快捷键')
    })
  }
  
  // Windows/Linux 专属快捷键
  if (!isMac) {
    globalShortcut.register('Control+Shift+I', () => {
      console.log('Windows/Linux 专属快捷键')
    })
  }
}
 
// 应用退出时注销快捷键
app.on('will-quit', () => {
  globalShortcut.unregisterAll()
})

4.3 文件系统与路径

4.3.1 路径分隔符

关键原则: 永远不要手动拼接路径字符串! 始终使用 Node.js 的 path 模块。

javascript
import path from 'node:path'
 
// ❌ 错误示例
const wrongPath1 = __dirname + '\\images\\icon.png'      // Windows 硬编码
const wrongPath2 = __dirname + '/images/icon.png'        // Unix 硬编码
const wrongPath3 = __dirname + path.sep + 'images' + path.sep + 'icon.png' // 冗长
 
// ✅ 正确示例
const correctPath = path.join(__dirname, 'images', 'icon.png')
// Windows: C:\project\images\icon.png
// macOS/Linux: /project/images/icon.png
 
// 获取文件名和扩展名
path.basename('/path/to/file.txt')        // 'file.txt'
path.extname('/path/to/file.txt')         // '.txt'
path.dirname('/path/to/file.txt')         // '/path/to'
 
// 解析相对路径
path.resolve('config', 'app.json')        // 绝对路径

4.3.2 应用数据目录

使用 app.getPath() 获取平台标准路径:

javascript
import { app } from 'electron'
 
// 常用路径
const appData = app.getPath('appData')      // 应用数据目录
const userData = app.getPath('userData')    // 用户数据目录
const temp = app.getPath('temp')            // 临时文件目录
const desktop = app.getPath('desktop')      // 桌面目录
const documents = app.getPath('documents')  // 文档目录
const downloads = app.getPath('downloads')  // 下载目录
const home = app.getPath('home')            // 用户主目录
 
// 完整路径映射表
API 名称macOSWindowsLinux
appData~/Library/Application Support%APPDATA%~/.config~/.local/share
userData~/Library/Application Support/[AppName]%APPDATA%\[AppName]~/.config/[AppName]
temp~/Library/Caches%TEMP%/tmp
home~%USERPROFILE%~
desktop~/Desktop%USERPROFILE%\Desktop~/Desktop
documents~/Documents%USERPROFILE%\Documents~/Documents
downloads~/Downloads%USERPROFILE%\Downloads~/Downloads

实际应用示例:

javascript
import { app } from 'electron'
import path from 'node:path'
import fs from 'node:fs'
 
// 确保应用数据目录存在
function ensureDataDir() {
  const dataDir = path.join(app.getPath('userData'), 'data')
  if (!fs.existsSync(dataDir)) {
    fs.mkdirSync(dataDir, { recursive: true })
  }
  return dataDir
}
 
// 保存配置文件
function saveConfig(config) {
  const configPath = path.join(app.getPath('userData'), 'config.json')
  fs.writeFileSync(configPath, JSON.stringify(config, null, 2))
}
 
// 读取配置文件
function loadConfig() {
  const configPath = path.join(app.getPath('userData'), 'config.json')
  try {
    return JSON.parse(fs.readFileSync(configPath, 'utf-8'))
  } catch (error) {
    return {} // 返回默认配置
  }
}

4.4 系统托盘 (Tray)

4.4.1 图标格式要求

平台推荐格式尺寸要求特殊要求
macOSPNG (Template Image)16x16, 32x32 (@2x)文件名需以 Template 结尾
WindowsICO16x16, 32x32, 256x256支持多尺寸嵌入
LinuxPNG22x22, 24x24取决于桌面环境

托盘图标实现:

javascript
import { Tray, nativeImage, Menu } from 'electron'
import path from 'node:path'
import { isMac, isWindows } from './platform'
 
let tray = null
 
function createTray() {
  // 根据平台选择图标
  const iconPath = getTrayIconPath()
  const icon = nativeImage.createFromPath(iconPath)
  
  // macOS 使用模板图片
  if (isMac) {
    icon.setTemplateImage(true)
  }
  
  tray = new Tray(icon)
  
  // 设置托盘菜单
  const contextMenu = Menu.buildFromTemplate([
    { label: '显示窗口', click: () => showMainWindow() },
    { label: '设置', click: () => openSettings() },
    { type: 'separator' },
    { label: '退出', click: () => app.quit() }
  ])
  
  tray.setToolTip('我的应用')
  tray.setContextMenu(contextMenu)
  
  // 点击托盘图标显示窗口 (macOS)
  if (isMac) {
    tray.on('click', () => showMainWindow())
  }
}
 
function getTrayIconPath() {
  const iconsDir = path.join(__dirname, 'assets', 'icons')
  
  if (isMac) {
    // macOS 模板图片
    return path.join(iconsDir, 'trayIconTemplate.png')
  } else if (isWindows) {
    // Windows ICO 文件
    return path.join(iconsDir, 'trayIcon.ico')
  } else {
    // Linux PNG
    return path.join(iconsDir, 'trayIcon.png')
  }
}
 
// macOS 深色模式切换时更新图标
if (isMac) {
  nativeTheme.on('updated', () => {
    if (tray) {
      // 重新创建托盘图标
      tray.destroy()
      createTray()
    }
  })
}

4.5 应用更新与分发

4.5.1 macOS 签名与公证

签名与公证流程:

plaintext
┌──────────────────────────────────────────────────────────┐
│               macOS 应用分发流程                          │
├──────────────────────────────────────────────────────────┤
│                                                           │
│  1. 开发完成                                              │
│       ↓                                                   │
│  2. 代码签名 (Code Signing)                               │
│     • 使用 Apple 开发者证书                               │
│     • 签名所有可执行文件和框架                            │
│       ↓                                                   │
│  3. 公证 (Notarization)                                   │
│     • 上传到 Apple 服务器                                 │
│     • 等待 Apple 验证                                     │
│     • 获取公证票据                                        │
│       ↓                                                   │
│  4. Staple (可选)                                         │
│     • 将公证票据附加到应用包                              │
│       ↓                                                   │
│  5. 分发                                                  │
│     • 用户下载后可直接运行                                │
│     • 不会被 Gatekeeper 阻止                              │
│                                                           │
└──────────────────────────────────────────────────────────┘

electron-builder 配置:

json
// electron-builder.json
{
  "mac": {
    "target": [
      {
        "target": "dmg",
        "arch": ["x64", "arm64", "universal"]
      }
    ],
    "category": "public.app-category.productivity",
    "hardenedRuntime": true,
    "entitlements": "build/entitlements.mac.plist",
    "entitlementsInherit": "build/entitlements.mac.plist",
    "gatekeeperAssess": false
  },
  "afterSign": "build/notarize.js"
}
javascript
// build/notarize.js
const { notarize } = require('@electron/notarize')
 
exports.default = async function notarizing(context) {
  const { electronPlatformName, appOutDir } = context
  
  if (electronPlatformName !== 'darwin') {
    return
  }
  
  const appName = context.packager.appInfo.productFilename
  
  return await notarize({
    appBundleId: 'com.yourcompany.yourapp',
    appPath: `${appOutDir}/${appName}.app`,
    appleId: process.env.APPLE_ID,
    appleIdPassword: process.env.APPLE_ID_PASSWORD,
    teamId: process.env.APPLE_TEAM_ID
  })
}

4.5.2 Windows 代码签名

electron-builder 配置:

json
// electron-builder.json
{
  "win": {
    "target": [
      {
        "target": "nsis",
        "arch": ["x64", "ia32"]
      },
      {
        "target": "portable",
        "arch": ["x64"]
      }
    ],
    "certificateFile": "path/to/certificate.pfx",
    "certificatePassword": process.env.WIN_CERT_PASSWORD,
    "signingHashAlgorithms": ["sha256"],
    "sign": null, // 使用默认签名工具
    "publisherName": "Your Company Name"
  }
}

4.5.3 开发者快速修复

macOS 开发阶段遇到"文件已损坏"提示时:

bash
# 移除应用的隔离属性 (仅用于开发测试)
sudo xattr -rd com.apple.quarantine /Applications/YourApp.app
 
# 查看应用的签名信息
codesign -dv --verbose=4 /Applications/YourApp.app
 
# 验证公证状态
spctl --assess --verbose /Applications/YourApp.app

4.6 原生模块与 C++ 扩展

4.6.1 Node-API 与 N-API

使用 Node-API 可以编写跨平台的原生模块,避免为每个 Electron 版本重新编译:

javascript
// binding.gyp
{
  "targets": [
    {
      "target_name": "addon",
      "sources": ["addon.cc"],
      "include_dirs": ["<!@(node -p \"require('node-addon-api').include\")"],
      "dependencies": ["<!(node -p \"require('node-addon-api').gyp\")"],
      "cflags!": ["-fno-exceptions"],
      "cflags_cc!": ["-fno-exceptions"],
      "conditions": [
        ["OS=='mac'", {
          "xcode_settings": {
            "GCC_ENABLE_CPP_EXCEPTIONS": "YES",
            "CLANG_CXX_LIBRARY": "libc++",
            "MACOSX_DEPLOYMENT_TARGET": "10.13"
          }
        }],
        ["OS=='win'", {
          "msvs_settings": {
            "VCCLCompilerTool": {
              "ExceptionHandling": 1
            }
          }
        }]
      ]
    }
  ]
}

4.6.2 平台特定代码

javascript
// addon.cc
#include <napi.h>
 
#ifdef _WIN32
#include <windows.h>
#elif defined(__APPLE__)
#include <ApplicationServices/ApplicationServices.h>
#else
#include <gtk/gtk.h>
#endif
 
Napi::Value GetPlatformInfo(const Napi::CallbackInfo& info) {
  Napi::Env env = info.Env();
  Napi::Object result = Napi::Object::New(env);
  
  #ifdef _WIN32
  result.Set("platform", "windows");
  result.Set("hasDarkMode", true); // Windows 10+
  #elif defined(__APPLE__)
  result.Set("platform", "macos");
  // 获取 macOS 深色模式状态
  CFStringRef appearance = CFStringCreateWithCString(
    kCFAllocatorDefault, 
    "AppleInterfaceStyle", 
    kCFStringEncodingUTF8
  );
  CFPropertyListRef value = CFPreferencesCopyAppValue(
    appearance, 
    kCFPreferencesAnyApplication
  );
  result.Set("hasDarkMode", value != nullptr);
  #else
  result.Set("platform", "linux");
  result.Set("hasDarkMode", false);
  #endif
  
  return result;
}
 
Napi::Object Init(Napi::Env env, Napi::Object exports) {
  exports.Set("getPlatformInfo", Napi::Function::New(env, GetPlatformInfo));
  return exports;
}
 
NODE_API_MODULE(addon, Init)

5. API 接口说明

5.1 平台相关 API 速查

5.1.1 仅 macOS 可用的 API

API功能使用场景
app.setActivationPolicy(policy)设置应用激活策略控制应用是否在 Dock 显示
app.dock.hide() / app.dock.show()隐藏/显示 Dock 图标后台运行应用
app.dock.setMenu(menu)设置 Dock 菜单快速操作菜单
app.dock.bounce()Dock 图标弹跳提醒用户注意
BrowserWindow.setTrafficLightPosition()设置交通灯位置自定义标题栏
systemPreferences.getUserDefault()获取系统偏好设置读取系统设置

5.1.2 仅 Windows 可用的 API

API功能使用场景
app.setUserTasks(tasks)设置跳转列表任务栏快捷操作
app.getJumpListSettings()获取跳转列表设置自定义跳转列表
BrowserWindow.setThumbarButtons()设置任务栏缩略图按钮媒体控制
BrowserWindow.setAppDetails()设置应用详情任务栏显示信息

5.1.3 跨平台 API 平台差异

APImacOS 行为Windows 行为Linux 行为
dialog.showOpenDialog()原生文件选择器原生文件选择器GTK 文件选择器
Notification系统通知中心Windows 通知libnotify
clipboard系统剪贴板系统剪贴板X11 剪贴板
shell.openExternal()默认浏览器默认浏览器默认浏览器

5.2 平台判断 API 封装

javascript
// src/main/platform-features.js
import { app, systemPreferences, nativeTheme } from 'electron'
import { isMac, isWindows, isLinux } from './platform'
 
export const platformFeatures = {
  // 深色模式支持
  supportsDarkMode: true,
  
  // 获取当前主题
  getTheme() {
    return nativeTheme.shouldUseDarkColors ? 'dark' : 'light'
  },
  
  // 监听主题变化
  onThemeChange(callback) {
    nativeTheme.on('updated', () => {
      callback(this.getTheme())
    })
  },
  
  // macOS 专属功能
  mac: {
    // 设置 Dock 菜单
    setDockMenu(menu) {
      if (isMac) {
        app.dock.setMenu(menu)
      }
    },
    
    // 隐藏 Dock 图标
    hideFromDock() {
      if (isMac) {
        app.dock.hide()
      }
    },
    
    // 设置激活策略
    setActivationPolicy(policy) {
      if (isMac) {
        app.setActivationPolicy(policy)
      }
    }
  },
  
  // Windows 专属功能
  windows: {
    // 设置任务栏按钮
    setThumbarButtons(window, buttons) {
      if (isWindows) {
        window.setThumbarButtons(buttons)
      }
    },
    
    // 设置跳转列表
    setUserTasks(tasks) {
      if (isWindows) {
        app.setUserTasks(tasks)
      }
    }
  }
}

6. 配置参数详解

6.1 BrowserWindow 配置

6.1.1 平台相关配置项

javascript
import { BrowserWindow } from 'electron'
import { isMac, isWindows } from './platform'
 
const windowConfig = {
  // 通用配置
  width: 1200,
  height: 800,
  minWidth: 800,
  minHeight: 600,
  webPreferences: {
    nodeIntegration: false,
    contextIsolation: true,
    preload: path.join(__dirname, 'preload.js')
  },
  
  // macOS 专属配置
  ...(isMac && {
    titleBarStyle: 'hiddenInset',  // 隐藏标题栏但保留交通灯
    trafficLightPosition: { x: 16, y: 16 }, // 交通灯位置
    vibrancy: 'under-window',      // 毛玻璃效果
    transparent: true              // 允许透明
  }),
  
  // Windows 专属配置
  ...(isWindows && {
    frame: true,                   // 标准窗口框架
    autoHideMenuBar: true,        // 自动隐藏菜单栏
    backgroundColor: '#ffffff',    // 背景色
    // Windows 11 圆角
    ...(process.platform === 'win32' && {
      autoHideMenuBar: true
    })
  }),
  
  // Linux 专属配置
  ...(isLinux && {
    frame: true,
    icon: path.join(__dirname, 'assets/icon.png')
  })
}
 
const mainWindow = new BrowserWindow(windowConfig)

6.1.2 titleBarStyle 选项详解

效果推荐平台
'default'标准标题栏全平台
'hidden'隐藏标题栏,交通灯在左上角macOS
'hiddenInset'隐藏标题栏,交通灯内嵌macOS
'customButtonsOnHover'鼠标悬停时显示自定义按钮macOS

6.2 electron-builder 配置

json
// electron-builder.json
{
  "$schema": "https://raw.githubusercontent.com/electron-userland/electron-builder/master/packages/app-builder-lib/scheme.json",
  
  "appId": "com.yourcompany.yourapp",
  "productName": "Your App",
  "copyright": "Copyright © 2024 Your Company",
  
  "directories": {
    "output": "dist",
    "buildResources": "build"
  },
  
  "files": [
    "dist/**/*",
    "node_modules/**/*",
    "package.json"
  ],
  
  "mac": {
    "target": [
      {
        "target": "dmg",
        "arch": ["x64", "arm64", "universal"]
      }
    ],
    "category": "public.app-category.productivity",
    "icon": "build/icon.icns",
    "hardenedRuntime": true,
    "entitlements": "build/entitlements.mac.plist",
    "entitlementsInherit": "build/entitlements.mac.plist",
    "gatekeeperAssess": false,
    "extendInfo": {
      "NSCameraUsageDescription": "用于视频通话",
      "NSMicrophoneUsageDescription": "用于语音通话"
    }
  },
  
  "win": {
    "target": [
      {
        "target": "nsis",
        "arch": ["x64", "ia32"]
      }
    ],
    "icon": "build/icon.ico",
    "certificateFile": "build/certificate.pfx",
    "certificatePassword": "${env.WIN_CERT_PASSWORD}",
    "publisherName": "Your Company Name",
    "verifyUpdateCodeSignature": false
  },
  
  "nsis": {
    "oneClick": false,
    "allowToChangeInstallationDirectory": true,
    "installerIcon": "build/installer.ico",
    "uninstallerIcon": "build/uninstaller.ico",
    "installerHeaderIcon": "build/headerIcon.bmp",
    "createDesktopShortcut": true,
    "createStartMenuShortcut": true,
    "shortcutName": "Your App"
  },
  
  "linux": {
    "target": ["AppImage", "deb", "rpm"],
    "category": "Utility",
    "icon": "build/icons/",
    "maintainer": "your@email.com",
    "desktop": {
      "Name": "Your App",
      "Comment": "Your app description",
      "Categories": "Utility;Application;"
    }
  },
  
  "dmg": {
    "contents": [
      {
        "x": 130,
        "y": 220
      },
      {
        "x": 410,
        "y": 220,
        "type": "link",
        "path": "/Applications"
      }
    ]
  },
  
  "appImage": {
    "systemIntegration": "ask"
  }
}

6.3 entitlements.mac.plist

xml
<!-- build/entitlements.mac.plist -->
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
    <!-- 启用 Hardened Runtime -->
    <key>com.apple.security.cs.allow-jit</key>
    <true/>
    
    <!-- 允许无签名的可执行代码 -->
    <key>com.apple.security.cs.allow-unsigned-executable-memory</key>
    <true/>
    
    <!-- 禁用库验证 -->
    <key>com.apple.security.cs.disable-library-validation</key>
    <true/>
    
    <!-- 网络访问 -->
    <key>com.apple.security.network.client</key>
    <true/>
    
    <!-- 文件读写 -->
    <key>com.apple.security.files.user-selected.read-write</key>
    <true/>
    
    <!-- 摄像头访问 -->
    <key>com.apple.security.device.camera</key>
    <true/>
    
    <!-- 麦克风访问 -->
    <key>com.apple.security.device.audio-input</key>
    <true/>
</dict>
</plist>

7. 调试与测试策略

7.1 平台特定调试技巧

7.1.1 日志记录

javascript
// src/main/logger.js
import { app } from 'electron'
import path from 'node:path'
import fs from 'node:fs'
 
class Logger {
  constructor() {
    const logDir = path.join(app.getPath('userData'), 'logs')
    if (!fs.existsSync(logDir)) {
      fs.mkdirSync(logDir, { recursive: true })
    }
    this.logFile = path.join(logDir, 'app.log')
  }
  
  log(level, message, ...args) {
    const timestamp = new Date().toISOString()
    const platform = process.platform
    const logMessage = `[${timestamp}] [${platform}] [${level.toUpperCase()}] ${message}\n`
    
    fs.appendFileSync(this.logFile, logMessage)
    console.log(`[${level.toUpperCase()}]`, message, ...args)
  }
  
  info(message, ...args) {
    this.log('info', message, ...args)
  }
  
  error(message, ...args) {
    this.log('error', message, ...args)
  }
  
  warn(message, ...args) {
    this.log('warn', message, ...args)
  }
}
 
export const logger = new Logger()

7.1.2 平台检测调试

javascript
// 开发环境下的平台信息打印
if (process.env.NODE_ENV === 'development') {
  console.log('=== 平台信息 ===')
  console.log('平台:', process.platform)
  console.log('架构:', process.arch)
  console.log('Electron 版本:', process.versions.electron)
  console.log('Node 版本:', process.versions.node)
  console.log('Chrome 版本:', process.versions.chrome)
  console.log('用户数据目录:', app.getPath('userData'))
  console.log('================')
}

7.2 跨平台测试策略

7.2.1 手动测试检查清单

markdown
## 功能测试清单
 
### 窗口管理
- [ ] 窗口可以正常打开和关闭
- [ ] 最小化、最大化、还原功能正常
- [ ] macOS: 关闭窗口后应用仍在运行
- [ ] Windows/Linux: 关闭窗口后应用退出
- [ ] macOS: 点击 Dock 图标可以重新打开窗口
- [ ] 全屏模式工作正常
 
### 菜单与快捷键
- [ ] 应用菜单正常显示
- [ ] 快捷键正常工作
- [ ] macOS: 应用菜单在顶部显示
- [ ] Windows/Linux: 菜单栏在窗口内
 
### 文件操作
- [ ] 文件对话框正常工作
- [ ] 文件可以正常保存和读取
- [ ] 路径在不同平台正确处理
 
### 系统集成
- [ ] 系统托盘图标正常显示
- [ ] 通知功能正常工作
- [ ] 应用更新功能正常
 
### 性能
- [ ] 应用启动时间合理
- [ ] 内存使用正常
- [ ] CPU 使用正常

7.2.2 自动化测试

javascript
// tests/platform.test.js
import { expect } from 'chai'
import { isMac, isWindows, isLinux } from '../src/main/platform'
 
describe('平台检测', () => {
  it('应该正确检测当前平台', () => {
    const platformCount = [isMac, isWindows, isLinux].filter(Boolean).length
    expect(platformCount).to.equal(1, '应该只有一个平台为 true')
  })
  
  it('平台标识符应该正确', () => {
    if (isMac) {
      expect(process.platform).to.equal('darwin')
    } else if (isWindows) {
      expect(process.platform).to.equal('win32')
    } else if (isLinux) {
      expect(process.platform).to.equal('linux')
    }
  })
})

8. 最佳实践清单

8.1 代码组织

  • ✅ 将平台判断逻辑集中在 platform.js 模块
  • ✅ 使用 IPC 将渲染进程的平台请求转发给主进程
  • ✅ 避免在渲染进程中直接访问 Node.js API
  • ✅ 使用 path 模块处理所有路径操作

8.2 UI/UX 设计

  • ✅ 为不同平台设计符合其设计规范的界面
  • ✅ macOS: 遵循 Human Interface Guidelines
  • ✅ Windows: 遵循 Fluent Design System
  • ✅ 正确处理深色模式
  • ✅ 考虑不同平台的字体渲染差异

8.3 性能优化

  • ✅ 延迟加载平台特定模块
  • ✅ 避免不必要的平台判断
  • ✅ 缓存平台信息,避免重复计算

8.4 安全考虑

  • ✅ 验证所有用户输入
  • ✅ 正确处理文件权限
  • ✅ 使用 contextIsolation 和 nodeIntegration: false
  • ✅ 对敏感操作进行权限检查

8.5 测试与发布

  • ✅ 在所有目标平台上测试
  • ✅ 使用 CI/CD 自动化测试
  • ✅ 对应用进行代码签名
  • ✅ macOS 应用进行公证
  • ✅ 提供详细的发布说明

9. 常见问题解答 (FAQ)

Q1: 如何在 macOS 上实现点击 Dock 图标重新打开窗口?

A: 监听 app 模块的 activate 事件:

javascript
app.on('activate', () => {
  if (BrowserWindow.getAllWindows().length === 0) {
    createWindow()
  }
})

Q2: Windows 上如何避免 SmartScreen 警告?

A:

  1. 购买代码签名证书(EV 证书效果更好)
  2. 为安装包签名
  3. 随着下载量增加,建立信誉

Q3: macOS 上提示"文件已损坏"怎么办?

A: 开发阶段可使用以下命令:

bash
sudo xattr -rd com.apple.quarantine /Applications/YourApp.app

正式发布需要完成签名和公证流程。

Q4: 如何处理不同平台的快捷键?

A: 使用 Electron 提供的 CommandOrControl 修饰符:

javascript
globalShortcut.register('CommandOrControl+S', () => {
  // 跨平台保存快捷键
})

Q5: 如何在渲染进程中获取平台信息?

A: 通过 preload 脚本暴露:

javascript
// preload.js
contextBridge.exposeInMainWorld('platform', {
  isMac: process.platform === 'darwin',
  isWindows: process.platform === 'win32',
  isLinux: process.platform === 'linux'
})

Q6: Linux 上系统托盘图标显示异常怎么办?

A:

  1. 确保 PNG 格式图标尺寸正确(22x22 或 24x24)
  2. 检查桌面环境是否支持系统托盘
  3. 考虑使用 AppIndicator 替代方案

Q7: 如何实现跨平台的自动更新?

A: 使用 electron-updater:

javascript
import { autoUpdater } from 'electron-updater'
 
autoUpdater.checkForUpdatesAndNotify()

配置 electron-builder:

json
{
  "publish": {
    "provider": "github",
    "owner": "your-github-username",
    "repo": "your-repo"
  }
}

Q8: 如何处理不同平台的应用菜单?

A: 动态构建菜单模板,根据平台插入不同菜单项:

javascript
const template = [
  ...(isMac ? [{ label: app.name, submenu: [...] }] : []),
  { label: '文件', submenu: [...] }
]

Q9: macOS 上如何支持 Apple Silicon (M1/M2/M3)?

A: 使用 electron-builder 构建 universal 版本:

json
{
  "mac": {
    "target": ["universal"]
  }
}

Q10: 如何在 Windows 上请求管理员权限?

A: 在 electron-builder 配置中:

json
{
  "win": {
    "requestedExecutionLevel": "requireAdministrator"
  }
}

10. 平台特性对比总表

特性WindowsmacOSLinux
平台标识 (process.platform)'win32''darwin''linux'
路径分隔符 (path.sep)\//
窗口关闭行为关闭所有窗口后应用退出关闭所有窗口后应用继续运行关闭所有窗口后应用退出
应用菜单位置窗口内屏幕顶部全局菜单栏通常在窗口内
快捷键修饰符CtrlCommand (⌘)Ctrl
托盘图标格式.ico (推荐)PNG Template Image.png
应用数据目录%APPDATA%~/Library/Application Support~/.config
分发安全机制SmartScreenGatekeeper (需签名公证)包管理器/权限系统
代码签名可选(减少 SmartScreen 警告)必需可选
深色模式Windows 10+macOS 10.14+取决于桌面环境
通知系统Windows 通知通知中心libnotify
自动更新SquirrelSquirrel.MacAppImage/Flatpak

11. 参考资源

11.1 官方文档

11.2 平台设计指南

11.3 社区资源

11.4 相关工具


12. 附录

12.1 版本更新记录

版本日期主要变更编辑者
2.0.02024-12-23全面重构:添加系统架构图、API接口说明、配置参数详解、调试策略、FAQ等Claude
1.0.02025-12-05初始创建Gemini

12.2 贡献者

  • Gemini: 初始文档结构设计与内容撰写
  • Claude: 全面重构与补充、技术验证

12.3 许可证

本文档采用 CC BY-NC-SA 4.0 许可证。


💡 提示: 如果您在开发过程中遇到新的跨平台问题或有更好的解决方案,欢迎贡献您的经验,帮助完善本文档。