Electron
第十二章:原生模块与跨平台开发
第十二章:原生模块与跨平台开发
目录
跨平台差异全景
Electron 号称”一次编写,三端运行”,但三个操作系统在窗口行为、系统集成上有显著差异,不处理好这些差异,应用在某个平台上就会显得格格不入。
┌──────────────────────────────────────────────────────────────┐
│ macOS vs Windows vs Linux 核心差异 │
│ │
│ 维度 macOS Windows Linux │
│ ────────────────────────────────────────────────────────── │
│ 关闭窗口 应用继续运行 应用退出 应用退出 │
│ 应用生命周期 Dock 常驻 任务栏 + 托盘 依桌面环境 │
│ 菜单栏 屏幕顶部全局 窗口内部 窗口内部 │
│ 通知 通知中心 Toast libnotify │
│ 文件路径分隔 / \ / │
│ 快捷键修饰符 Cmd (⌘) Ctrl Ctrl │
│ 托盘图标 菜单栏图标 系统托盘 系统托盘 │
│ 窗口圆角 系统自带 无 依主题 │
└──────────────────────────────────────────────────────────────┘
窗口关闭 ≠ 退出:macOS 的特殊行为
macOS 用户习惯关闭所有窗口后应用仍在 Dock 中运行,点击 Dock 图标可以重新打开窗口。Windows 和 Linux 用户则期望关闭最后一个窗口等于退出应用。
macOS: 窗口关闭 → 应用仍在 Dock → 点击 Dock → 重新创建窗口
Win/Lin: 窗口关闭 → 应用完全退出(进程结束)
// main.js — 标准跨平台窗口关闭处理
app.on('window-all-closed', () => {
// macOS:不退出应用,保持 Dock 图标
if (process.platform !== 'darwin') {
app.quit();
}
});
// macOS:点击 Dock 图标时重新创建窗口
app.on('activate', () => {
if (BrowserWindow.getAllWindows().length === 0) {
createWindow();
}
});
Dock vs 任务栏 vs 系统托盘
┌──────────────────────────────────────────────────────────────┐
│ 系统托盘 / Dock 行为差异 │
│ │
│ macOS: │
│ • 菜单栏右侧 Tray 图标,建议用 Template Image(适配深色) │
│ • Dock 图标常驻,可设置右键菜单和角标 │
│ • 图标尺寸:16x16 @2x │
│ │
│ Windows: │
│ • 托盘在右下角,左键/右键均可触发菜单 │
│ • 图标格式 .ico,尺寸 16x16 / 32x32 │
│ • 支持气泡通知(balloon)和双击事件 │
│ │
│ Linux: │
│ • 托盘行为取决于桌面环境(GNOME / KDE / XFCE) │
│ • GNOME 默认隐藏托盘图标,需安装 AppIndicator 扩展 │
│ • 建议同时提供任务栏入口作为后备 │
└──────────────────────────────────────────────────────────────┘
// 跨平台托盘图标
function createTray() {
let iconPath;
if (process.platform === 'darwin') {
iconPath = path.join(__dirname, 'assets/tray-iconTemplate.png');
} else if (process.platform === 'win32') {
iconPath = path.join(__dirname, 'assets/tray-icon.ico');
} else {
iconPath = path.join(__dirname, 'assets/tray-icon.png');
}
const tray = new Tray(iconPath);
tray.setContextMenu(Menu.buildFromTemplate([
{ label: '显示窗口', click: () => mainWindow.show() },
{ type: 'separator' },
{ label: '退出', click: () => app.quit() },
]));
// Windows:双击托盘图标显示窗口
if (process.platform === 'win32') {
tray.on('double-click', () => mainWindow.show());
}
}
文件路径与系统目录
不同操作系统将用户数据、缓存、日志存放在完全不同的位置。硬编码路径是跨平台的大忌,必须使用 app.getPath()。
┌──────────────────────────────────────────────────────────────┐
│ app.getPath() 在三平台上的实际路径 │
│ │
│ 参数 macOS Windows │
│ ──────────────────────────────────────────────────────── │
│ userData ~/Library/App Support/X AppData\Roaming\X │
│ temp /var/folders/.../T/ AppData\Local\Temp │
│ desktop ~/Desktop ~\Desktop │
│ documents ~/Documents ~\Documents │
│ downloads ~/Downloads ~\Downloads │
│ logs ~/Library/Logs/X AppData\Roaming\X\logs │
│ cache ~/Library/Caches/X AppData\Local\X\Cache │
│ │
│ 参数 Linux │
│ ──────────────────────────────────────────────────────── │
│ userData ~/.config/X │
│ temp /tmp │
│ logs ~/.config/X/logs │
│ cache ~/.cache/X │
└──────────────────────────────────────────────────────────────┘
// 正确做法:始终用 app.getPath
const dbPath = path.join(app.getPath('userData'), 'data.db');
const logPath = path.join(app.getPath('logs'), 'app.log');
// 错误做法 ❌ — 硬编码路径
const dbPath = '/Users/xxx/Library/Application Support/MyApp/data.db';
路径分隔符
Windows 使用反斜杠 \,macOS/Linux 使用正斜杠 /
// 错误 ❌
const filePath = baseDir + '\\config\\settings.json';
// 正确 ✅
const filePath = path.join(baseDir, 'config', 'settings.json');
注意:URL 协议始终使用正斜杠,即使在 Windows 上
file:///C:/Users/xxx/app/index.html ✅
file:///C:\Users\xxx\app\index.html ❌
快捷键差异与 CmdOrCtrl
macOS 用 Cmd (⌘) 作为主修饰键,Windows/Linux 用 Ctrl。Electron 提供 CmdOrCtrl 修饰符来统一处理。
┌──────────────────────────────────────────────────────────────┐
│ 快捷键映射关系 │
│ │
│ 功能 macOS Windows / Linux │
│ ────────────────────────────────────────────────────── │
│ 复制 Cmd+C Ctrl+C │
│ 保存 Cmd+S Ctrl+S │
│ 撤销 Cmd+Z Ctrl+Z │
│ 设置 Cmd+, Ctrl+, │
│ 退出 Cmd+Q Alt+F4 │
│ 隐藏 Cmd+H 无对应 │
│ │
│ Electron 方案: │
│ 'CmdOrCtrl+S' → macOS 映射 Cmd+S,其他平台映射 Ctrl+S │
└──────────────────────────────────────────────────────────────┘
// 菜单中使用 accelerator
const menuTemplate = [
{
label: '文件',
submenu: [
{ label: '保存', accelerator: 'CmdOrCtrl+S', click: () => saveFile() },
{ label: '另存为', accelerator: 'CmdOrCtrl+Shift+S', click: () => saveFileAs() },
],
},
];
// macOS 特有菜单:应用名称菜单(第一个菜单项)
if (process.platform === 'darwin') {
menuTemplate.unshift({
label: app.getName(),
submenu: [
{ role: 'about' },
{ type: 'separator' },
{ role: 'hide' }, // Cmd+H(macOS 独有)
{ role: 'hideOthers' }, // Cmd+Opt+H
{ type: 'separator' },
{ role: 'quit' }, // Cmd+Q
],
});
}
// 全局快捷键
globalShortcut.register('CmdOrCtrl+Shift+I', () => {
mainWindow.webContents.toggleDevTools();
});
其他修饰符映射:
CmdOrCtrl macOS → Cmd Win/Linux → Ctrl
Alt macOS → Option Win/Linux → Alt
Super/Meta macOS → Cmd Win/Linux → Win 键
原生模块
什么时候需要原生模块
Electron 运行在 Node.js 之上,大部分逻辑用 JS 就能完成。但有些场景必须使用 C/C++ 编写的原生模块:
┌──────────────────────────────────────────────────────────────┐
│ 何时需要原生模块 │
│ │
│ 场景 典型模块 │
│ ────────────────────────────────────────────── │
│ 嵌入式数据库(高性能 SQLite) better-sqlite3 │
│ 系统密钥链 / 凭据管理 keytar │
│ 终端模拟器 node-pty │
│ 串口通信 serialport │
│ 系统级文件监听 fsevents (macOS) │
│ 图像处理 sharp │
│ 硬件访问(USB / HID) node-hid、usb │
│ 操作系统 API 调用 ffi-napi、edge-js │
│ │
│ 判断原则: │
│ ✓ JS 性能不够 / 需调用 OS C API → 用原生模块 │
│ ✗ 有纯 JS 替代方案且性能可接受 → 优先用 JS │
└──────────────────────────────────────────────────────────────┘
原生模块技术栈演进
┌──────────────────────────────────────────────────────────────┐
│ Node.js 原生模块技术栈 │
│ │
│ 时代 技术 特点 │
│ ────────────────────────────────────────────── │
│ 早期 NAN V8 API 直接调用,每个 Node │
│ 版本都要重新编译,维护成本高 │
│ │
│ 过渡 node-addon-api NAN 的 C++ 友好封装 │
│ 仍需针对版本编译 │
│ │
│ 现在 N-API (推荐) ABI 稳定,跨版本兼容 │
│ 编译一次,多版本运行 │
│ │
│ N-API 的核心优势: │
│ 传统:源码 → 编译 for Node18 / Node20 / Node22(各一份) │
│ N-API:源码 → 编译一次 → addon.node → 全版本通用 │
└──────────────────────────────────────────────────────────────┘
electron-rebuild — 解决 ABI 兼容问题
Electron 内置的 Node.js 与系统 Node.js 的 ABI 不同。直接 npm install 的原生模块是针对系统 Node 编译的,在 Electron 中会崩溃。
┌──────────────────────────────────────────────────────────────┐
│ 为什么需要 electron-rebuild │
│ │
│ npm install better-sqlite3 │
│ │ │
│ ▼ │
│ 用系统 Node (v20) 头文件编译 → ABI = Node v20 │
│ │ │
│ ▼ │
│ Electron 加载时: │
│ ┌──────────────────────────────────────────┐ │
│ │ Error: The module was compiled against │ │
│ │ a different Node.js version using │ │
│ │ NODE_MODULE_VERSION 115. This version │ │
│ │ requires NODE_MODULE_VERSION 119. │ │
│ └──────────────────────────────────────────┘ │
│ │
│ 解决:npx electron-rebuild │
│ → 用 Electron 内置 Node 的头文件重新编译 → ✅ 加载成功 │
└──────────────────────────────────────────────────────────────┘
# 安装
npm install -D @electron/rebuild
# 每次安装原生模块后执行
npx electron-rebuild
# 推荐:在 package.json 中自动化
# "scripts": { "postinstall": "electron-rebuild" }
# 仅重新编译指定模块
npx electron-rebuild --only better-sqlite3
构建工具集成:
Electron Forge → 打包时自动 rebuild,无需额外配置
electron-builder → 自带 rebuild(npmRebuild: true)
常见原生模块一览
┌───────────────────┬─────────────────────────────────────────┐
│ 模块 │ 用途 │
├───────────────────┼─────────────────────────────────────────┤
│ better-sqlite3 │ 同步 SQLite,本地数据库首选 │
│ keytar │ 系统密钥链(Keychain / Credential Vault)│
│ node-pty │ 伪终端,内置终端模拟器 │
│ sharp │ 高性能图像处理(缩放、裁剪、转格式) │
│ serialport │ 串口通信,IoT 设备连接 │
│ node-hid │ USB HID 设备访问 │
│ fsevents │ macOS 高效文件监听 │
│ robotjs │ 模拟鼠标键盘操作 │
└───────────────────┴─────────────────────────────────────────┘
平台特有功能
macOS:Touch Bar
MacBook Pro 的 Touch Bar 可以显示自定义控件(注意:新款 MacBook 已移除,但存量设备仍多)。
const { TouchBar } = require('electron');
const { TouchBarButton, TouchBarSpacer } = TouchBar;
const playBtn = new TouchBarButton({
label: '▶ 播放',
backgroundColor: '#4CAF50',
click: () => mainWindow.webContents.send('transport', 'play'),
});
const stopBtn = new TouchBarButton({
label: '⏹ 停止',
click: () => mainWindow.webContents.send('transport', 'stop'),
});
mainWindow.setTouchBar(new TouchBar({
items: [playBtn, new TouchBarSpacer({ size: 'small' }), stopBtn],
}));
macOS:Dock 菜单与角标
if (process.platform === 'darwin') {
app.dock.setMenu(Menu.buildFromTemplate([
{ label: '新建窗口', click: () => createWindow() },
]));
app.dock.setBadge('3'); // 角标显示未读数
app.dock.bounce('informational'); // 弹跳一次吸引注意
}
Windows:缩略图工具栏(Thumbnail Toolbar)
任务栏预览窗口中显示操作按钮,适合音乐播放器等场景。
if (process.platform === 'win32') {
mainWindow.setThumbarButtons([
{
tooltip: '上一曲',
icon: nativeImage.createFromPath('assets/prev.png'),
click: () => mainWindow.webContents.send('transport', 'prev'),
},
{
tooltip: '播放',
icon: nativeImage.createFromPath('assets/play.png'),
click: () => mainWindow.webContents.send('transport', 'play'),
},
{
tooltip: '下一曲',
icon: nativeImage.createFromPath('assets/next.png'),
click: () => mainWindow.webContents.send('transport', 'next'),
},
]);
}
Windows:Jump List(跳转列表)
任务栏右键菜单,显示最近文件和自定义任务。
if (process.platform === 'win32') {
app.setJumpList([
{
type: 'custom',
name: '最近的项目',
items: [
{ type: 'task', title: '打开项目 A',
program: process.execPath, args: '--open-project=a' },
],
},
{
type: 'custom',
name: '快捷操作',
items: [
{ type: 'task', title: '新建窗口',
program: process.execPath, args: '--new-window' },
],
},
]);
}
Windows:Toast 通知 & 进度条
// Windows 10+ 需要设置 AppUserModelID 才能正常显示通知
if (process.platform === 'win32') {
app.setAppUserModelId('com.mycompany.myapp');
}
new Notification({
title: '下载完成',
body: '文件已保存到下载文件夹',
icon: path.join(__dirname, 'assets/icon.png'),
}).show();
// 任务栏进度条(Windows & macOS 均支持)
mainWindow.setProgressBar(0.5); // 50%
mainWindow.setProgressBar(-1); // 移除
Linux:Unity Launcher & libnotify
// Ubuntu Unity / GNOME 启动器角标
if (process.platform === 'linux') {
app.setBadgeCount(5); // 需要 .desktop 文件注册
}
Linux 通知系统:
Electron Notification API → libnotify → 桌面通知守护进程
注意事项:
• 必须安装 libnotify-bin
• 某些桌面环境样式差异大
• 部分功能(action button)不一定支持
条件编码模式
process.platform 分支
// 模式一:简单 if 分支
if (process.platform === 'darwin') { /* macOS */ }
else if (process.platform === 'win32') { /* Windows */ }
else { /* Linux */ }
// 模式二:布尔常量(推荐,代码更清晰)
const isMac = process.platform === 'darwin';
const isWin = process.platform === 'win32';
const isLinux = process.platform === 'linux';
const menuTemplate = [
...(isMac ? [{ label: app.getName(), submenu: [{ role: 'about' }] }] : []),
{
label: '文件',
submenu: [
isMac ? { role: 'close' } : { role: 'quit' },
],
},
];
平台特定文件模式
当平台差异代码较多时,拆分到独立文件中:
src/main/platform/
├── index.js # 根据平台加载对应文件
├── darwin.js # macOS 专属
├── win32.js # Windows 专属
└── linux.js # Linux 专属
// platform/index.js — 统一导出
module.exports = require(`./${process.platform}`);
// platform/darwin.js
module.exports = {
setupSystemIntegration(mainWindow) {
const { app, Menu } = require('electron');
app.dock.setMenu(Menu.buildFromTemplate([
{ label: '新建窗口', click: () => { /* ... */ } },
]));
},
};
// platform/win32.js
module.exports = {
setupSystemIntegration(mainWindow) {
const { app } = require('electron');
app.setAppUserModelId('com.mycompany.myapp');
// Jump List、Thumbnail Toolbar 等
},
};
// main/index.js — 使用
const platform = require('./platform');
platform.setupSystemIntegration(mainWindow);
选择性依赖
// 某些原生模块只在特定平台有效(如 fsevents 仅 macOS)
// package.json: "optionalDependencies": { "fsevents": "^2.3.3" }
let fsevents;
try {
fsevents = require('fsevents');
} catch {
// 非 macOS 平台,静默忽略
}
跨平台 CI 构建矩阵
使用 GitHub Actions 的 matrix 策略在三个平台上同时构建和测试:
# .github/workflows/build.yml
name: Build & Test
on:
push:
branches: [main]
pull_request:
jobs:
build:
strategy:
fail-fast: false # 一个平台失败不影响其他平台继续
matrix:
include:
- os: macos-latest
platform: mac
- os: windows-latest
platform: win
- os: ubuntu-latest
platform: linux
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
cache: 'npm'
- run: npm ci
# Linux 安装原生模块编译依赖
- name: 安装 Linux 构建依赖
if: matrix.platform == 'linux'
run: |
sudo apt-get update
sudo apt-get install -y libgtk-3-dev libnotify-dev \
libnss3 libxss1 libasound2-dev
- run: npx electron-rebuild
name: Rebuild 原生模块
- run: npx vitest run
name: 单元测试
# E2E(Linux 需要 xvfb)
- name: E2E 测试 (Linux)
if: matrix.platform == 'linux'
run: xvfb-run --auto-servernum npm run test:e2e
- name: E2E 测试 (macOS / Windows)
if: matrix.platform != 'linux'
run: npm run test:e2e
- name: 构建应用
run: npx electron-builder --${{ matrix.platform }}
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- uses: actions/upload-artifact@v4
with:
name: dist-${{ matrix.platform }}
path: dist/*.{dmg,exe,AppImage,deb}
构建矩阵执行流程:
git push / PR
│
▼
┌──────────────────────────────────────────────────────┐
│ GitHub Actions — 三平台并行 │
│ │
│ ┌────────────┐ ┌────────────┐ ┌────────────┐ │
│ │ macOS │ │ Windows │ │ Linux │ │
│ │ npm ci │ │ npm ci │ │ npm ci │ │
│ │ rebuild │ │ rebuild │ │ apt-get │ │
│ │ test │ │ test │ │ rebuild │ │
│ │ build │ │ build │ │ xvfb test │ │
│ │ → .dmg │ │ → .exe │ │ → .AppImage│ │
│ └────────────┘ └────────────┘ └────────────┘ │
│ │
│ ✅ 三平台全部通过 → 合并 PR / 发布 │
│ ❌ 任一平台失败 → 阻止合并 │
└──────────────────────────────────────────────────────┘
交叉编译注意事项
原生模块无法交叉编译!
❌ 在 macOS 上编译 Windows 的 .node 文件
❌ 在 Linux 上编译 macOS 的 .node 文件
原因:原生模块需要目标平台的编译工具链和头文件
解决方案:
1. CI 矩阵 — 在每个平台上分别构建(推荐)
2. prebuild — 预编译二进制,npm install 时自动下载
better-sqlite3、sharp 等已提供 prebuild
3. Docker — Linux 交叉编译不同架构(x86_64 / arm64)
常见跨平台坑位对照表
┌─────────────────────────────────────────────────────────────────┐
│ 跨平台常见坑位对照表 │
│ │
│ 坑位 表现 解决方案 │
│ ─────────────────────────────────────────────────────────── │
│ 路径分隔符 Win 用 \ 其他用 / path.join/resolve │
│ 文件名大小写 macOS/Win 不敏感 统一小写,CI 跑 Lin │
│ 行尾符号 Win: CRLF 其他: LF .gitattributes eol=lf│
│ 窗口关闭行为 macOS 关闭不退出 window-all-closed │
│ 菜单栏位置 macOS 屏幕顶部 unshift 应用名称菜单 │
│ 快捷键 macOS: Cmd 其他: Ctrl CmdOrCtrl 统一 │
│ 原生模块 ABI Electron ≠ 系统 Node electron-rebuild │
│ 托盘图标格式 macOS: Template PNG 按平台提供不同图标 │
│ Win: .ico Lin: PNG │
│ 通知 API 三平台底层不同 Electron Notification│
│ 文件权限 Win chmod 无效 平台分支处理 │
│ Shell 命令 Win: cmd/PS 其他: sh cross-env 等工具 │
│ 字体渲染 三平台差异大 避免依赖特定字体 │
│ 开机自启 各平台方式不同 setLoginItemSettings │
│ 拖放文件路径 Win 返回 \ path.normalize │
│ 系统代理 三平台配置不同 session.setProxy │
└─────────────────────────────────────────────────────────────────┘
实践建议
┌──────────────────────────────────────────────────────────────┐
│ 原生模块与跨平台开发最佳实践 │
│ │
│ 1. 从第一天就在三平台上测试 │
│ 不要等到发布前才在 Windows/Linux 上跑一下 │
│ CI 矩阵是最低成本的三平台覆盖方案 │
│ │
│ 2. 绝对不要硬编码路径 │
│ 用 path.join / path.resolve 拼接路径 │
│ 用 app.getPath() 获取系统目录 │
│ │
│ 3. 统一使用 CmdOrCtrl │
│ 菜单 accelerator 和全局快捷键都用 CmdOrCtrl │
│ 不要写死 Cmd 或 Ctrl │
│ │
│ 4. 原生模块必须 electron-rebuild │
│ 在 postinstall 脚本中自动执行 │
│ CI 中也要执行 rebuild │
│ 优先选择提供 prebuild 的模块 │
│ │
│ 5. 平台逻辑集中管理 │
│ 小差异用 process.platform 三元表达式 │
│ 大差异抽到 platform/ 目录的独立文件 │
│ 避免平台判断散落在代码各处 │
│ │
│ 6. 优先使用 Electron 跨平台 API │
│ Notification、dialog、Tray 等 API 已封装好差异 │
│ 只在 API 不够用时才走平台分支 │
│ │
│ 7. 原生模块选型原则 │
│ 有纯 JS 方案 → 优先用纯 JS(零编译问题) │
│ 必须原生 → 选 N-API 实现(ABI 稳定) │
│ 必须原生 → 选有 prebuild 的模块(免编译) │
│ │
│ 8. .gitattributes 必须配置 │
│ * text=auto eol=lf │
│ *.{png,ico,jpg} binary │
│ 避免行尾符号问题导致跨平台文件哈希不一致 │
│ │
│ 9. 文件名大小写 │
│ macOS/Win 不区分大小写,Linux 区分 │
│ import './MyComponent' 在 Linux 上可能找不到 │
│ 统一使用 kebab-case 或全小写文件名 │
│ │
│ 10. 无框窗口需三平台验证 │
│ titleBarStyle / titleBarOverlay 在各平台效果不同 │
│ macOS 的交通灯按钮、Windows 的系统按钮需分别处理 │
└──────────────────────────────────────────────────────────────┘
本章小结
┌──────────────────────────────────────────────────────────────┐
│ 本章核心要点 │
│ │
│ 跨平台差异: │
│ • macOS 关闭窗口 ≠ 退出,需处理 window-all-closed │
│ • macOS 菜单在屏幕顶部,需 unshift 应用名称菜单 │
│ • 快捷键统一用 CmdOrCtrl │
│ • 路径用 path.join + app.getPath,禁止硬编码 │
│ │
│ 原生模块: │
│ • OS 底层 API 或性能敏感场景才用原生模块 │
│ • electron-rebuild 解决 ABI 不兼容 │
│ • 优先选 N-API 实现 + prebuild 的模块 │
│ │
│ 平台特有功能: │
│ • macOS: Touch Bar、Dock 菜单与角标 │
│ • Windows: Thumb Bar、Jump List、Toast 通知 │
│ • Linux: Unity Launcher 角标、libnotify │
│ │
│ 条件编码: │
│ • 小差异 → process.platform 判断 │
│ • 大差异 → platform/ 目录独立文件 │
│ │
│ CI 构建: │
│ • GitHub Actions matrix 覆盖三平台 │
│ • 原生模块不能交叉编译,必须在目标平台构建 │
└──────────────────────────────────────────────────────────────┘
上一章:签名、公证与生产发布 ←