Electron 跨平台兼容性权威指南
1. 引言
Electron 以其 "一次编写,处处运行" 的承诺,极大地简化了桌面应用的开发。然而,尽管它在抽象底层差异方面做得非常出色,Windows、macOS 和 Linux 三大操作系统之间固有的平台差异依然是开发者必须面对的挑战。
1.1 文档目标
本指南旨在:
- 全面梳理 Electron 开发中常见的跨平台兼容性问题
- 提供经过验证的最佳实践和解决方案
- 建立清晰的平台适配开发流程
- 帮助开发者规避常见陷阱
1.2 适用读者
- Electron 初学者:了解基础的平台差异概念
- 中级开发者:掌握跨平台适配技巧
- 高级开发者:深入理解底层机制和性能优化
2. 系统架构与核心概念
2.1 多进程架构概述
理解 Electron 的多进程架构是处理平台差异的基石。
┌─────────────────────────────────────────────────────────────┐
│ 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 通信最佳实践
// 主进程 - 处理平台相关的文件操作
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:
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,需通过以下方式获取平台信息:
// 方法 1: 通过 preload 脚本暴露
// preload.js
import { contextBridge } from 'electron'
import { getPlatformInfo } from './platform'
contextBridge.exposeInMainWorld('electronPlatform', {
...getPlatformInfo()
})
// 渲染进程中使用
console.log(window.electronPlatform.platformName) // 'macOS' / 'Windows' / 'Linux'// 方法 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}')`
)
})
}/* 样式适配示例 */
.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 窗口关闭行为
差异说明:
┌─────────────┬──────────────────────────────────┐
│ 平台 │ 默认行为 │
├─────────────┼──────────────────────────────────┤
│ Windows │ 关闭所有窗口 → 应用退出 │
│ Linux │ 关闭所有窗口 → 应用退出 │
│ macOS │ 关闭所有窗口 → 应用继续运行 │
│ │ (Dock 栏保持活动状态) │
└─────────────┴──────────────────────────────────┘完整解决方案:
// 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 | 右侧 | 关闭/最小化/最大化 | 关闭窗口通常退出应用 |
自定义标题栏实现:
// 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 -->
<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>// 窗口控制函数
const { ipcRenderer } = require('electron')
function minimizeWindow() {
ipcRenderer.send('window-minimize')
}
function maximizeWindow() {
ipcRenderer.send('window-maximize')
}
function closeWindow() {
ipcRenderer.send('window-close')
}// 主进程处理窗口控制
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 窗口状态管理
// 保存和恢复窗口状态
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 应用菜单
平台差异:
┌─────────────┬──────────────────────────────────┐
│ 平台 │ 菜单位置与行为 │
├─────────────┼──────────────────────────────────┤
│ macOS │ 全局菜单栏,固定在屏幕顶部 │
│ │ 第一个菜单为应用菜单 │
│ Windows │ 菜单栏在窗口标题栏下方 │
│ Linux │ 取决于桌面环境 │
└─────────────┴──────────────────────────────────┘动态菜单构建:
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 修饰符 | macOS | Windows/Linux |
|---|---|---|
CommandOrControl / CmdOrCtrl | Command (⌘) | Control (Ctrl) |
Option / Alt | Option (⌥) | Alt |
Shift | Shift (⇧) | Shift |
Super / Meta | Command (⌘) | Windows 键 / Super 键 |
最佳实践示例:
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 模块。
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() 获取平台标准路径:
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 名称 | macOS | Windows | Linux |
|---|---|---|---|
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 |
实际应用示例:
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 图标格式要求
| 平台 | 推荐格式 | 尺寸要求 | 特殊要求 |
|---|---|---|---|
| macOS | PNG (Template Image) | 16x16, 32x32 (@2x) | 文件名需以 Template 结尾 |
| Windows | ICO | 16x16, 32x32, 256x256 | 支持多尺寸嵌入 |
| Linux | PNG | 22x22, 24x24 | 取决于桌面环境 |
托盘图标实现:
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 签名与公证
签名与公证流程:
┌──────────────────────────────────────────────────────────┐
│ macOS 应用分发流程 │
├──────────────────────────────────────────────────────────┤
│ │
│ 1. 开发完成 │
│ ↓ │
│ 2. 代码签名 (Code Signing) │
│ • 使用 Apple 开发者证书 │
│ • 签名所有可执行文件和框架 │
│ ↓ │
│ 3. 公证 (Notarization) │
│ • 上传到 Apple 服务器 │
│ • 等待 Apple 验证 │
│ • 获取公证票据 │
│ ↓ │
│ 4. Staple (可选) │
│ • 将公证票据附加到应用包 │
│ ↓ │
│ 5. 分发 │
│ • 用户下载后可直接运行 │
│ • 不会被 Gatekeeper 阻止 │
│ │
└──────────────────────────────────────────────────────────┘electron-builder 配置:
// 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"
}// 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 配置:
// 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 开发阶段遇到"文件已损坏"提示时:
# 移除应用的隔离属性 (仅用于开发测试)
sudo xattr -rd com.apple.quarantine /Applications/YourApp.app
# 查看应用的签名信息
codesign -dv --verbose=4 /Applications/YourApp.app
# 验证公证状态
spctl --assess --verbose /Applications/YourApp.app4.6 原生模块与 C++ 扩展
4.6.1 Node-API 与 N-API
使用 Node-API 可以编写跨平台的原生模块,避免为每个 Electron 版本重新编译:
// 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 平台特定代码
// 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 平台差异
| API | macOS 行为 | Windows 行为 | Linux 行为 |
|---|---|---|---|
dialog.showOpenDialog() | 原生文件选择器 | 原生文件选择器 | GTK 文件选择器 |
Notification | 系统通知中心 | Windows 通知 | libnotify |
clipboard | 系统剪贴板 | 系统剪贴板 | X11 剪贴板 |
shell.openExternal() | 默认浏览器 | 默认浏览器 | 默认浏览器 |
5.2 平台判断 API 封装
// 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 平台相关配置项
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 配置
// 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
<!-- 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 日志记录
// 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 平台检测调试
// 开发环境下的平台信息打印
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 手动测试检查清单
## 功能测试清单
### 窗口管理
- [ ] 窗口可以正常打开和关闭
- [ ] 最小化、最大化、还原功能正常
- [ ] macOS: 关闭窗口后应用仍在运行
- [ ] Windows/Linux: 关闭窗口后应用退出
- [ ] macOS: 点击 Dock 图标可以重新打开窗口
- [ ] 全屏模式工作正常
### 菜单与快捷键
- [ ] 应用菜单正常显示
- [ ] 快捷键正常工作
- [ ] macOS: 应用菜单在顶部显示
- [ ] Windows/Linux: 菜单栏在窗口内
### 文件操作
- [ ] 文件对话框正常工作
- [ ] 文件可以正常保存和读取
- [ ] 路径在不同平台正确处理
### 系统集成
- [ ] 系统托盘图标正常显示
- [ ] 通知功能正常工作
- [ ] 应用更新功能正常
### 性能
- [ ] 应用启动时间合理
- [ ] 内存使用正常
- [ ] CPU 使用正常7.2.2 自动化测试
// 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 事件:
app.on('activate', () => {
if (BrowserWindow.getAllWindows().length === 0) {
createWindow()
}
})Q2: Windows 上如何避免 SmartScreen 警告?
A:
- 购买代码签名证书(EV 证书效果更好)
- 为安装包签名
- 随着下载量增加,建立信誉
Q3: macOS 上提示"文件已损坏"怎么办?
A: 开发阶段可使用以下命令:
sudo xattr -rd com.apple.quarantine /Applications/YourApp.app正式发布需要完成签名和公证流程。
Q4: 如何处理不同平台的快捷键?
A: 使用 Electron 提供的 CommandOrControl 修饰符:
globalShortcut.register('CommandOrControl+S', () => {
// 跨平台保存快捷键
})Q5: 如何在渲染进程中获取平台信息?
A: 通过 preload 脚本暴露:
// preload.js
contextBridge.exposeInMainWorld('platform', {
isMac: process.platform === 'darwin',
isWindows: process.platform === 'win32',
isLinux: process.platform === 'linux'
})Q6: Linux 上系统托盘图标显示异常怎么办?
A:
- 确保 PNG 格式图标尺寸正确(22x22 或 24x24)
- 检查桌面环境是否支持系统托盘
- 考虑使用 AppIndicator 替代方案
Q7: 如何实现跨平台的自动更新?
A: 使用 electron-updater:
import { autoUpdater } from 'electron-updater'
autoUpdater.checkForUpdatesAndNotify()配置 electron-builder:
{
"publish": {
"provider": "github",
"owner": "your-github-username",
"repo": "your-repo"
}
}Q8: 如何处理不同平台的应用菜单?
A: 动态构建菜单模板,根据平台插入不同菜单项:
const template = [
...(isMac ? [{ label: app.name, submenu: [...] }] : []),
{ label: '文件', submenu: [...] }
]Q9: macOS 上如何支持 Apple Silicon (M1/M2/M3)?
A: 使用 electron-builder 构建 universal 版本:
{
"mac": {
"target": ["universal"]
}
}Q10: 如何在 Windows 上请求管理员权限?
A: 在 electron-builder 配置中:
{
"win": {
"requestedExecutionLevel": "requireAdministrator"
}
}10. 平台特性对比总表
| 特性 | Windows | macOS | Linux |
|---|---|---|---|
平台标识 (process.platform) | 'win32' | 'darwin' | 'linux' |
路径分隔符 (path.sep) | \ | / | / |
| 窗口关闭行为 | 关闭所有窗口后应用退出 | 关闭所有窗口后应用继续运行 | 关闭所有窗口后应用退出 |
| 应用菜单位置 | 窗口内 | 屏幕顶部全局菜单栏 | 通常在窗口内 |
| 快捷键修饰符 | Ctrl | Command (⌘) | Ctrl |
| 托盘图标格式 | .ico (推荐) | PNG Template Image | .png |
| 应用数据目录 | %APPDATA% | ~/Library/Application Support | ~/.config |
| 分发安全机制 | SmartScreen | Gatekeeper (需签名公证) | 包管理器/权限系统 |
| 代码签名 | 可选(减少 SmartScreen 警告) | 必需 | 可选 |
| 深色模式 | Windows 10+ | macOS 10.14+ | 取决于桌面环境 |
| 通知系统 | Windows 通知 | 通知中心 | libnotify |
| 自动更新 | Squirrel | Squirrel.Mac | AppImage/Flatpak |
11. 参考资源
11.1 官方文档
11.2 平台设计指南
11.3 社区资源
- Electron Fiddle - 快速实验 Electron API
- Electron 示例应用
- awesome-electron
11.4 相关工具
- electron-rebuild - 重建原生模块
- electron-packager - 打包工具
- electron-notarize - macOS 公证
- electron-osx-sign - macOS 签名
12. 附录
12.1 版本更新记录
| 版本 | 日期 | 主要变更 | 编辑者 |
|---|---|---|---|
| 2.0.0 | 2024-12-23 | 全面重构:添加系统架构图、API接口说明、配置参数详解、调试策略、FAQ等 | Claude |
| 1.0.0 | 2025-12-05 | 初始创建 | Gemini |
12.2 贡献者
- Gemini: 初始文档结构设计与内容撰写
- Claude: 全面重构与补充、技术验证
12.3 许可证
本文档采用 CC BY-NC-SA 4.0 许可证。
💡 提示: 如果您在开发过程中遇到新的跨平台问题或有更好的解决方案,欢迎贡献您的经验,帮助完善本文档。