Electron MenuItemBadge 深度解析:在 macOS 菜单项上显示系统级角标的完整机制
【免费下载链接】electron:electron: Build cross-platform desktop apps with JavaScript, HTML, and CSS项目地址: https://gitcode.com/GitHub_Trending/el/electron
本篇基于 Electron 仓库的docs/api/structures/menu-item-badge.md官方文档展开,系统讲解MenuItemBadge对象的全部字段(type、count、content)、取值约束与本地化规则,并结合lib/browser/api/menu-item.ts的参数校验逻辑与shell/browser/ui/cocoa/electron_menu_controller.mm中到NSMenuItemBadge的转换实现,帮助你在 macOS 应用菜单中正确配置、动态更新和移除菜单角标。
一、MenuItemBadge 是什么
MenuItemBadge是 ElectronMenu/MenuItemAPI 中的一个结构体(structure)对象,用于在macOS菜单项的标签旁显示角标(badge)。它可以呈现两种形态:
- 系统样式角标:通过
alerts、updates、new-items三种预定义类型之一,由 macOS 系统自动本地化并处理复数形式(pluralization); - 自定义文本角标:使用
none类型搭配content字符串,显示任意自定义内容,此时本地化与复数形式需开发者自行处理。
该能力对应 macOS 14(Sonoma)引入的NSMenuItemBadgeAppKit API,因此文档在 docs/api/menu-item.md 中明确标注:仅 macOS 14 及以上版本可用。
二、字段详解与取值约束
以下是 docs/api/structures/menu-item-badge.md 中定义的完整字段说明:
1.type(string,可选)
- 可选值:
'alerts'、'updates'、'new-items'、'none'; - 默认值:
'none'。
三种预定义类型分别对应 AppKit 中不同语义的系统角标(警报、更新、新条目)。使用预定义类型(非none)时,系统会自动为角标做本地化和复数形式处理,例如根据用户语言显示“3 个更新”或“1 update”这类本地化文案。
2.count(number,可选)
- 角标显示的条目数量;
- 对
alerts、updates、new-items三种类型必填; - 不能与
none类型同时使用。
3.content(string,可选)
- 显示在角标中的自定义字符串;
- 仅可与
none类型一起使用,且此时为必填。
原文档特别强调了一条本地化规则:
如果你使用预定义角标类型(非
none),系统会替你完成本地化和复数处理;如果你创建自定义角标字符串,你需要自己完成该字符串的本地化与复数处理。
三、主进程侧的参数校验:lib/browser/api/menu-item.ts
文档中的约束在 Electron 主进程 JS 层被严格实现。lib/browser/api/menu-item.ts 定义了合法类型白名单和validateBadge校验函数:
const badgeTypes = ['alerts', 'updates', 'new-items', 'none']; const validateBadge = (badge: any) => { if (badge == null) return; if (typeof badge !== 'object') { throw new TypeError('badge must be a MenuItemBadge object'); } const type = badge.type ?? 'none'; if (!badgeTypes.includes(type)) { throw new TypeError(`Invalid badge type '${type}': must be one of ${badgeTypes.join(', ')}`); } if (type === 'none') { if (typeof badge.content !== 'string') { throw new TypeError("badge.content must be a string when badge.type is 'none'"); } if (badge.count != null) { throw new TypeError("badge.count cannot be used when badge.type is 'none'"); } } else { if (!Number.isInteger(badge.count) || badge.count < 0) { throw new TypeError(`badge.count must be a non-negative integer when badge.type is '${type}'`); } if (badge.content != null) { throw new TypeError("badge.content can only be used when badge.type is 'none'"); } } };从这段源码可以确认几个比文档更细的约束:
badge必须是对象:传入非对象(非null/undefined)会直接抛出TypeError;type省略时按'none'处理:所以none类型下content是实际必填项,只传badge: {}会因content不是字符串而报错;count必须是非负整数:Number.isInteger(badge.count) && badge.count >= 0,浮点数或负数都会被拒绝;count与content互斥:none类型下不允许出现count,预定义类型下不允许出现content。
此外,badge属性只在process.platform === 'darwin'分支上被定义——这意味着在 Windows 和 Linux 上MenuItem实例根本没有badge属性,从源码结构看这是对该 API macOS 专属定位的直接体现。
四、badge 的动态更新与原生同步机制
1. JS 层的 setter:改完立即下推原生菜单
MenuItem构造器中通过Object.defineProperty为badge安装 getter/setter(见 lib/browser/api/menu-item.ts 第 73-86 行):
if (process.platform === 'darwin') { validateBadge(options.badge); let badgeValue = options.badge ?? undefined; Object.defineProperty(this, 'badge', { get: () => badgeValue, set: (newValue) => { validateBadge(newValue); badgeValue = newValue ?? undefined; // Push the change to the native item if this item is already in a menu. if (this.menu) { const index = this.menu.getIndexOfCommandId(this.commandId); if (index !== -1) this.menu.setBadge(index, badgeValue ?? null); } }, enumerable: true }); }关键行为:
- 动态可改:docs/api/menu-item.md 指出
menuItem.badge可以被动态修改,设置为undefined即移除角标; - 即时下推:如果该菜单项已经挂在某个菜单上(
this.menu存在),setter 会通过内部方法menu.setBadge(index, badgeValue ?? null)把变更实时推送到原生NSMenuItem,无需重建整个Menu。移除时传null,与 C++ 侧“无 badge”语义对应。
setBadge由 shell/browser/api/electron_api_menu.cc 中的.SetMethod("setBadge", &Menu::SetBadge)暴露,其内部声明见 typings/internal-electron.d.ts:
setBadge(index: number, badge: MenuItemBadge | null): void;2. C++ 模型层:按 commandId 存储
原生模型 shell/browser/ui/electron_menu_model.cc 中维护了一个以command_id为键的badges_映射:
void ElectronMenuModel::SetBadge(size_t index, std::optional<Badge> badge) { const int command_id = GetCommandIdAt(index); if (badge) badges_[command_id] = std::move(*badge); else badges_.erase(command_id); } bool ElectronMenuModel::GetBadgeAt(size_t index, Badge* badge) const { int command_id = GetCommandIdAt(index); const auto iter = badges_.find(command_id); if (iter != badges_.end()) { *badge = iter->second; return true; } return false; }SetBadge传入空值即从映射中删除该条目,这正是 JS 侧设置undefined后角标消失的底层实现。
3. macOS 层:转换为 NSMenuItemBadge
最终的角标创建发生在 shell/browser/ui/cocoa/electron_menu_controller.mm:
// Convert a Badge to an NSMenuItemBadge, or nil if it has nothing to show. NSMenuItemBadge* CreateBadge(const electron::ElectronMenuModel::Badge& badge) { if (badge.type == "none") { if (!badge.content) return nil; return [[NSMenuItemBadge alloc] initWithString:base::SysUTF8ToNSString(*badge.content)]; } const NSInteger count = badge.count.value_or(0); if (badge.type == "alerts") return [NSMenuItemBadge alertsWithCount:count]; if (badge.type == "updates") return [NSMenuItemBadge updatesWithCount:count]; if (badge.type == "new-items") return [NSMenuItemBadge newItemsWithCount:count]; return nil; }这段代码把 JS 字段与 AppKit 工厂方法一一映射:
type值 | 调用的 AppKit 方法 | 使用的字段 |
|---|---|---|
none | initWithString: | content(UTF-8 转 NSString) |
alerts | alertsWithCount: | count(缺省取 0) |
updates | updatesWithCount: | count(缺省取 0) |
new-items | newItemsWithCount: | count(缺省取 0) |
菜单构建与刷新路径中,ElectronMenuController通过model->GetBadgeAt(index, &badge)查询当前项的角标并赋值给item.badge(无角标时为nil)。这也说明角标状态由模型层按commandId持久保存,菜单重新构建时不会丢失。
五、完整使用示例
在菜单模板中静态声明角标
const { Menu } = require('electron'); const template = [ { label: 'My App', submenu: [ { label: 'Preferences…', accelerator: 'CmdOrCtrl+,' }, { label: 'Inbox', // 系统样式角标:显示 “3 updates”(系统自动本地化 + 复数处理) badge: { type: 'updates', count: 3 }, click: () => console.log('open inbox') }, { label: 'Drafts', // 自定义文本角标:本地化与复数需自行处理 badge: { type: 'none', content: 'New' } }, { type: 'separator' } ] } ]; Menu.setApplicationMenu(Menu.buildFromTemplate(template));动态更新与移除
const item = appMenu.items[3]; // 假设是 “Inbox” 项 // 更新数量 item.badge = { type: 'new-items', count: 7 }; // 切换为自定义文本 item.badge = { type: 'none', content: '1,024' }; // 移除角标 item.badge = undefined;由于 setter 内部会即时调用menu.setBadge(index, ...),上述改动无需重建Menu,对已弹出的应用菜单实时生效(下一次菜单展开时由ElectronMenuController按GetBadgeAt重新渲染)。
常见错误对照
结合 lib/browser/api/menu-item.ts 的validateBadge实现,以下写法会抛出TypeError:
| 写法 | 报错原因 |
|---|---|
badge: 'alerts' | badge must be a MenuItemBadge object,必须是对象 |
badge: { type: 'foo', count: 1 } | 非法 type,必须是alerts、updates、new-items、none之一 |
badge: { type: 'none' } | none类型要求content为字符串 |
badge: { type: 'none', count: 2 } | count不能与none同时使用 |
badge: { type: 'updates', count: 1.5 } | count必须是非负整数 |
badge: { type: 'updates', content: 'x' } | content只能搭配none使用 |
六、限制与注意事项
- 平台限制:仅 macOS 14 及以上。JS 层仅在
process.platform === 'darwin'时定义badge属性,非 macOS 平台上访问该属性会得到undefined; - Dock 菜单不显示角标:docs/api/menu-item.md 明确说明,badge 不会出现在 Dock 菜单(
dock.setMenu(menu))中,但同一菜单项在应用菜单里正常显示角标; - 本地化责任划分:预定义类型由系统负责本地化与复数形式;
none类型的自定义字符串则由开发者自行完成本地化和复数处理,这一点在 docs/api/structures/menu-item-badge.md 末尾被特别强调; - 校验时机:无论是构造
MenuItem时通过options.badge传入,还是运行时给menuItem.badge赋值,都会先经过validateBadge,非法组合在 JS 层即被拒绝,不会到达原生代码。
七、小结
MenuItemBadge以三个字段(type/count/content)把 macOSNSMenuItemBadge的能力暴露给 Electron 应用:预定义类型换取系统的本地化与复数能力,none类型换取完全自定义的显示内容。整条链路为:JS 层validateBadge校验(lib/browser/api/menu-item.ts)→Menu.setBadge下推(shell/browser/api/electron_api_menu.cc)→ElectronMenuModel按commandId存储(shell/browser/ui/electron_menu_model.cc)→CreateBadge转换为NSMenuItemBadge(shell/browser/ui/cocoa/electron_menu_controller.mm)。掌握这条链路与各字段互斥约束,即可在 macOS 应用菜单中正确落地系统级角标功能。
【免费下载链接】electron:electron: Build cross-platform desktop apps with JavaScript, HTML, and CSS项目地址: https://gitcode.com/GitHub_Trending/el/electron
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考