Electron Dock 菜单、右键菜单与菜单事件回传
概述
上一节解决了顶部应用菜单,这一节继续把菜单系统往更贴近真实桌面软件的地方扩展:macOS Dock 菜单和页面内右键菜单。课程先说明 Dock 菜单和顶部菜单思路几乎一样,只是挂载入口换成 app.dock.setMenu();接着提醒 Dock 菜单不适合做快捷键入口,而 Windows 上对应概念更接近 Jump List,平台差异要尊重。后半段讲右键菜单时,再次印证了 Electron 的核心分层:页面触发意图、主进程负责 popup() 原生菜单、preload 做受控桥接,且菜单点击回传页面仍然走 IPC 那条熟链路。
学习目标
- 理解 Dock 菜单和顶部菜单“入口不同、思路相同”:都是先
buildFromTemplate()再挂载,只是挂载方法换成app.dock.setMenu()。 - 知道 Dock 菜单不是快捷键主承载入口,配上去的
accelerator不会像顶部菜单那样工作。 - 明白 Windows 上与 Dock 菜单对应的是 Jump List,平台入口差异要尊重,不能假设所有平台都有同样入口。
- 掌握右键菜单的分层:渲染进程发“显示菜单”意图,主进程用
menu.popup()弹出原生菜单。 - 记住右键菜单的打开与点击回传,依然要经过 preload 受控暴露,并走 IPC 链回到页面。
一、菜单系统从“有菜单”推进到更完整的桌面入口
上一节主要解决顶部应用菜单,这一节则开始往更贴近桌面软件习惯的地方扩展:macOS Dock 菜单和页面内右键菜单。这一步非常有意义,因为真实桌面应用里用户最常接触的并不一定是顶部菜单,而是 Dock 图标右键弹出的菜单、页面内部右键弹出的上下文菜单。所以这一节其实是在把菜单系统从“有菜单”推进到“桌面端常见菜单入口更完整”。
type ElectronMenuEntry =
| "application-menu"
| "dock-menu"
| "context-menu"后面学这几种菜单时,要始终带着“它属于哪个入口层级”去看。桌面端菜单不只是顶部导航,课程这一节是在继续补足“像真正桌面软件”的交互入口。
二、Dock 菜单:入口不同,思路相同
课程里先讲 Dock 菜单,这个能力非常符合 macOS 用户习惯:右键应用图标弹出一个快捷操作菜单。实现上课程特意说明了一个让人安心的点:它和前面的顶部菜单非常像,核心仍然是定义 template、Menu.buildFromTemplate()、最后挂到对应入口上。只是这次挂载的方法不再是 Menu.setApplicationMenu(),而是 app.dock.setMenu()。
const dockMenu = Menu.buildFromTemplate(dockTemplate)
app.whenReady().then(() => {
if (process.platform === "darwin")
app.dock.setMenu(dockMenu)
})Dock 菜单是 macOS 特有的体验入口,所以要先判断平台。这类判断和前面 window-all-closed、activate 的平台差异思路是一脉相承的。课程这里的重点其实是“入口不同,思路相同”。
三、Dock 菜单不适合做快捷键入口
课程在 Dock 菜单里特意试了给某个菜单项配 accelerator,然后发现它并不像顶部菜单那样把快捷键展示和行为完整接起来。这一点非常值得记,因为它说明桌面端不同菜单入口虽然模板写法相近,但交互职责不完全一样:顶部菜单很适合挂快捷键,用户也天然会在这里预期看到快捷键;Dock 菜单更像快捷操作入口,但不是快捷键展示和绑定的主要承载位。
const dockTemplate = [
{
label: "Menu 1",
accelerator: "CommandOrControl+1",
click() {
console.log("click menu")
},
},
]课程顺手给出的经验是:更深的快捷键能力,后面更推荐用全局快捷键去做。不要把顶部菜单的快捷键心智完全照搬到 Dock 菜单;这里不是说 Dock 菜单不能配,而是在提醒它不是主要的快捷键入口。
四、平台差异:Windows 上对应的是 Jump List
课程讲完 Dock 菜单后,没有停在 macOS,而是顺手补了一句:Windows 上对应的更像 Jump List。这很重要,因为它再次强化了一个桌面端认知:平台之间并不是所有交互入口都一一对应。macOS 的 Dock 菜单和 Windows 的 Jump List,在“用户右键应用图标时给出快捷操作”层面很像,但 API、命名、平台习惯都不完全一样。
type PlatformMenuExtension =
| "dock-menu"
| "jump-list"课程虽然没深挖 Windows Jump List,但把它点出来本身就很有价值,因为这能帮助建立“桌面端不是统一大平地、平台入口差异要尊重”的意识。Dock 菜单不是所有平台都能照搬的能力。
五、右键菜单的控制权依然在主进程
课程后半段讲右键菜单,这是一个非常典型的 Electron 分层案例。表面上看右键动作发生在页面里,似乎应该全部在渲染层解决,但真正的菜单弹出还是走主进程里的原生菜单实例。所以课程的结构是:页面里监听右键或点击、通过 preload + IPC 发“请显示菜单”的意图、主进程用 menu.popup() 把菜单弹出来。
ipcMain.on("show-context-menu", (event) => {
const win = BrowserWindow.fromWebContents(event.sender)
if (!win)
return
popupMenu.popup({ window: win })
})这再一次说明:页面擅长感知交互,主进程擅长调系统能力,右键菜单只是这个原则的又一次体现。真正桌面端原生交互,很多都绕不开主进程收口。
六、右键菜单也要经过 preload 做受控桥接
课程在做右键菜单时,并没有直接让页面去碰 Electron API,而是继续用了前面一贯的做法:preload 里通过 contextBridge 暴露一个受控方法,例如 openMenu()。页面层只需要做一件事:在需要的时候调用 window.electronAPI.openMenu()。
// preload.js
contextBridge.exposeInMainWorld("electronAPI", {
openMenu: () => ipcRenderer.send("show-context-menu"),
onMenuClick: (callback) => {
ipcRenderer.on("menu-click", (_event, data) => callback(data))
},
})这样做的好处是页面不需要知道 IPC 频道名、也不直接碰高权限对象、逻辑边界继续清晰。这其实就是课程一直在重复强调的“桥接原则”,只是这次场景从文件系统、IPC、MessagePort 变成了菜单。到这里你应该能感受到:Electron 项目的安全设计和分层设计是同一件事。
七、菜单点击回传页面,同样回到 IPC 链
课程最后又往前推进了一步:菜单不只是显示,菜单点击之后页面也可能需要更新。例如右键点了某个菜单项,页面上要显示结果。这时课程采用的链路仍然和前面一致:菜单点击发生在主进程、主进程发消息回渲染进程、preload 再把这个回调透给页面、页面层接住后更新 UI。
{
label: "Menu 1",
click(_menuItem, win) {
win?.webContents.send("menu-click", "message from main")
},
}菜单点击回传本质上还是 IPC。这一步非常值得记,因为它说明菜单系统虽然看起来是“原生入口”,但一旦和页面联动,仍然回到了我们已经很熟的那套 Electron 分层通信模型:主进程菜单点击 → 发送消息 → preload 监听 → 页面处理。
八、这一节让菜单系统参与进页面交互
到这一节为止,Electron 菜单系统已经从“会建菜单”扩展到了“会让菜单参与页面交互”。它真正交付的是:不同菜单入口共享同一套模板思维但挂载入口和职责不同;右键菜单再次验证页面感知交互、主进程弹菜单、preload 做桥接的核心分层;菜单点击结果若影响页面,最终仍回到 IPC 模型。把这条链吃透,后续再做托盘菜单、命令面板等扩展都会顺理成章。
常见问题
| 问题 | 原因 | 解决方案 |
|---|---|---|
| 为什么 Dock 菜单在 Windows 上没效果 | Dock 菜单本来就是 macOS 入口 | 先做平台判断,不要假设所有平台都有同样入口 |
| 为什么右键菜单不能直接在页面里弹出来 | 原生菜单显示能力仍在主进程侧 | 页面只发意图,主进程负责 popup() |
| 菜单点击后页面没反应 | 主进程点击逻辑没有把事件再发回页面 | 继续走 webContents.send → preload → 页面 这条链 |
为什么 preload 里还要再包一层 openMenu | 为了不让页面直接接触高权限 IPC 能力 | 继续保持最小暴露原则 |
accelerator 配在 Dock 菜单上为什么没表现 | Dock 菜单不是快捷键主承载入口 | 把快捷键重点留给顶部菜单或后续全局快捷键能力 |