鸿蒙PC Electron 菜单栏重构与跨平台适配实践
我们采用了模块化 + 配置驱动分离配置与逻辑:将菜单项定义提取到独立的配置模块统一构建器:创建菜单构建器类,统一处理菜单构建逻辑平台适配:为不同平台提供适配方案,确保功能一致性✅ 代码可维护性大幅提升代码量减少 90%模块化设计,职责清晰易于扩展和修改✅ 跨平台兼容性完美解决所有平台菜单功能一致平台特有功能正确适配用户体验统一✅ 鸿蒙平台特殊需求满足正确检测鸿蒙平台隐藏不支持的"新窗口"功能其他功
鸿蒙PC Electron 菜单栏重构与跨平台适配实践
📖 前言
在将 Electron 应用移植到鸿蒙平台的过程中,我们遇到了菜单栏显示不一致的问题:macOS 平台上菜单栏功能完整,而鸿蒙平台上菜单项明显偏少。经过深入分析和重构,我们不仅解决了跨平台兼容性问题,还大幅提升了代码的可维护性。
本文将详细介绍菜单栏重构的过程、遇到的问题以及解决方案。
🎯 问题背景
1. 代码可维护性问题
原始的菜单栏实现存在以下问题:
-
代码集中在一个大函数中:
buildAppMenu()函数超过 500 行,包含所有菜单项定义 -
平台逻辑混杂:大量平台判断代码(
process.platform === 'darwin')散布在菜单定义中 -
代码重复:Dock 菜单和主菜单有重复的菜单项定义
-
难以扩展:添加新菜单项需要修改核心函数,容易引入错误
2. 跨平台兼容性问题
在鸿蒙平台上运行时,发现菜单栏存在以下问题:
-
菜单项缺失:应用菜单、窗口菜单等部分菜单项不显示
-
功能不一致:与 macOS 平台相比,菜单功能差异较大
-
用户体验差:用户无法访问某些功能
🔍 问题分析
问题 1:为什么鸿蒙平台菜单项少?
通过代码审查和 Electron 文档研究,我们发现问题的根本原因:
Electron 中某些菜单 role 是 macOS 特有的,在非 macOS 平台上会被忽略或隐藏:
// 这些 role 在非 macOS 平台上可能不显示
{
role: 'about', // macOS 系统对话框
role: 'services', // macOS Services 菜单
role: 'hide', // macOS 应用隐藏
role: 'hideothers', // macOS 隐藏其他应用
role: 'unhide', // macOS 显示所有应用
role: 'window', // macOS 窗口菜单
role: 'front' // macOS 窗口前置
}
当这些 role 在非 macOS 平台(包括鸿蒙)上使用时,Electron 会忽略它们,导致菜单项不显示。
问题 2:代码结构问题
原始代码结构:
function buildAppMenu(options = {}) {
// 500+ 行的代码
// 包含所有菜单项定义
// 大量平台判断逻辑
// 代码重复
}
这种结构导致:
-
难以定位特定菜单项
-
修改一个菜单项可能影响其他部分
-
平台逻辑与业务逻辑耦合
💡 解决方案
方案概述
我们采用了模块化 + 配置驱动的架构:
-
分离配置与逻辑:将菜单项定义提取到独立的配置模块
-
统一构建器:创建菜单构建器类,统一处理菜单构建逻辑
-
平台适配:为不同平台提供适配方案,确保功能一致性
架构设计
menu.js (主入口)
├── menuConfig.js (菜单配置模块)
│ ├── 菜单项定义
│ ├── 平台适配逻辑
│ └── 菜单项工厂函数
└── menuBuilder.js (菜单构建器)
├── 统一构建逻辑
└── 平台无关的菜单组装
🛠️ 实现细节
1. 创建菜单配置模块 (menuConfig.js)
将菜单项定义集中管理:
function createMenuConfig(getFormattedKeyMapEntry, handlers) {
// 标签页和任务相关操作
const tabTaskActions = [
{
label: l('appMenuNewTab'),
accelerator: getFormattedKeyMapEntry('addTab'),
click: (item, window, event) => {
if (!event || !event.triggeredByAccelerator) {
sendIPCToWindow(window, 'addTab')
}
}
},
// ... 其他菜单项
]
// 应用菜单(所有平台统一)
const appMenu = {
label: app.name,
submenu: [
// 平台适配的菜单项
]
}
return {
tabTaskActions,
appMenu,
fileMenu,
editMenu,
// ... 其他菜单配置
}
}
2. 创建菜单构建器 (menuBuilder.js)
统一构建逻辑,不区分平台:
class MenuBuilder {
constructor(menuConfig, options = {}) {
this.config = menuConfig
this.options = options
this.isSecondary = options.secondary || false
}
buildTemplate() {
const template = []
// 主菜单结构(所有平台统一)
template.push(this.config.appMenu) // 1. 应用菜单
template.push(this.buildFileMenu()) // 2. 文件菜单
template.push(this.buildEditMenu()) // 3. 编辑菜单
template.push(this.buildViewMenu()) // 4. 视图菜单
template.push(this.buildDeveloperMenu()) // 5. 开发者菜单
template.push(this.config.windowMenu) // 6. 窗口菜单
template.push(this.buildHelpMenu()) // 7. 帮助菜单
return template
}
}
3. 平台适配方案
方案 A:条件性使用 role
对于 macOS 特有的 role,在非 macOS 平台提供替代实现:
// 应用菜单中的"关于"项
{
label: l('appMenuAbout').replace('%n', app.name),
role: process.platform === 'darwin' ? 'about' : undefined,
click: process.platform !== 'darwin' ? (item, window) => {
// 非 macOS 平台:手动显示对话框
const info = [
'Min v' + app.getVersion(),
'Chromium v' + process.versions.chrome
]
electron.dialog.showMessageBox({
type: 'info',
title: l('appMenuAbout').replace('%n', app.name),
message: info.join('\n'),
buttons: [l('closeDialog')]
})
} : undefined
}
方案 B:使用 visible 属性
对于完全不需要的功能,使用 visible 属性隐藏:
// Services 菜单仅在 macOS 上显示
...(process.platform === 'darwin' ? [{
label: 'Services',
role: 'services',
submenu: []
}] : []),
// 隐藏其他应用功能仅在 macOS 上显示
{
label: l('appMenuHideOthers'),
accelerator: 'CmdOrCtrl+Alt+H',
role: process.platform === 'darwin' ? 'hideothers' : undefined,
visible: process.platform === 'darwin' // 仅在 macOS 上显示
}
方案 C:提供功能替代
对于 macOS 特有的功能,在非 macOS 平台提供替代实现:
// 隐藏应用功能
{
label: l('appMenuHide').replace('%n', app.name),
accelerator: 'CmdOrCtrl+H',
role: process.platform === 'darwin' ? 'hide' : undefined,
click: process.platform !== 'darwin' ? () => {
// 非 macOS 平台:最小化所有窗口
windows.getAll().forEach(win => win.minimize())
} : undefined
}
// 窗口前置功能
{
label: l('appMenuBringToFront'),
role: process.platform === 'darwin' ? 'front' : undefined,
click: process.platform !== 'darwin' ? (item, window) => {
// 非 macOS 平台:手动前置窗口
if (window) {
window.focus()
window.show()
} else {
windows.getAll().forEach(win => {
win.focus()
win.show()
})
}
} : undefined
}
4. 鸿蒙平台特殊处理
添加鸿蒙平台检测,隐藏"新窗口"菜单项:
// 检测是否为鸿蒙平台
function isHarmonyOSPlatform() {
const platform = process.platform
const harmonyEnv = process.env.HARMONY_OS || process.env.OHOS_PLATFORM
// 如果platform不是常见的darwin/win32/linux,可能是鸿蒙
const commonPlatforms = ['darwin', 'win32', 'linux']
if (!commonPlatforms.includes(platform)) {
console.log('检测到非标准平台,可能是鸿蒙系统:', platform)
return true
}
// 如果有鸿蒙环境变量
if (harmonyEnv) {
console.log('检测到鸿蒙环境变量:', harmonyEnv)
return true
}
return false
}
// 在菜单配置中使用
if (isHarmonyOSPlatform && isHarmonyOSPlatform()) {
// 鸿蒙平台:隐藏"新窗口"菜单项
console.log('鸿蒙平台:已隐藏"新窗口"菜单项')
} else {
// 非鸿蒙平台:添加"新窗口"菜单项
tabTaskActions.push({
label: l('appMenuNewWindow'),
// ...
})
}
📊 重构效果对比
代码量对比
| 指标 | 重构前 | 重构后 | 改善 |
|------|--------|--------|------|
| 主函数行数 | 500+ 行 | ~50 行 | ↓ 90% |
| 代码文件数 | 1 个 | 3 个 | 模块化 |
| 平台判断次数 | 20+ 处 | 0 处(统一) | 完全消除 |
可维护性提升
重构前:
// 所有代码在一个函数中
function buildAppMenu(options = {}) {
// 500+ 行代码
// 平台判断混杂
// 难以定位和修改
}
重构后:
// 清晰的模块结构
menu.js // 主入口,简洁
menuConfig.js // 配置集中管理
menuBuilder.js // 构建逻辑清晰
功能一致性
| 平台 | 重构前 | 重构后 |
|------|--------|--------|
| macOS | ✅ 完整功能 | ✅ 完整功能 |
| Windows | ⚠️ 部分功能缺失 | ✅ 完整功能 |
| Linux | ⚠️ 部分功能缺失 | ✅ 完整功能 |
| 鸿蒙 | ❌ 大量功能缺失 | ✅ 完整功能(除新窗口) |
🎓 最佳实践
1. 模块化设计
-
配置与逻辑分离:菜单项定义独立于构建逻辑
-
单一职责原则:每个模块只负责一个功能
-
易于测试:配置模块可以独立测试
2. 平台适配策略
-
优先使用标准 role:在支持的平台上使用 Electron 标准 role
-
提供替代实现:在不支持的平台上提供功能替代
-
优雅降级:使用
visible属性隐藏不支持的功能
3. 代码组织
-
集中管理配置:所有菜单项定义集中在一个模块
-
统一构建逻辑:使用构建器模式统一处理
-
清晰的注释:标注平台特性和注意事项
🚀 总结
通过这次重构,我们实现了以下目标:
- ✅ 代码可维护性大幅提升
-
代码量减少 90%
-
模块化设计,职责清晰
-
易于扩展和修改
- ✅ 跨平台兼容性完美解决
-
所有平台菜单功能一致
-
平台特有功能正确适配
-
用户体验统一
- ✅ 鸿蒙平台特殊需求满足
-
正确检测鸿蒙平台
-
隐藏不支持的"新窗口"功能
-
其他功能正常显示
关键收获
-
Electron role 的平台限制:某些 role 是平台特有的,需要提供替代方案
-
模块化的重要性:合理的模块划分能大幅提升代码质量
-
平台适配策略:条件性使用 role + 提供替代实现是最佳实践
后续优化方向
-
动态菜单:根据应用状态动态显示/隐藏菜单项
-
菜单项分组:进一步优化菜单结构
-
快捷键配置:支持用户自定义快捷键
-
多语言支持:完善菜单项的多语言翻译
📚 参考资料
更多推荐



所有评论(0)