本节摘要:文件对话框、剪贴板、系统通知、全局快捷键是桌面应用区别于网页的四项核心系统能力,全部由主进程模块提供、经 preload 白名单供页面调用。本节逐项讲正确用法与平台差异,给出统一的"能力接入模式",并用一个"快速笔记"功能把四项能力串成完整闭环。
先把共同模式说破:系统能力住在主进程,页面经白名单调用,权限心智围绕"最小暴露"设计。四项能力无一例外:
| 能力 | 主进程模块 | 关键 API | 平台差异要点 |
|---|---|---|---|
| 文件对话框 | dialog | showOpenDialog / showSaveDialog | 过滤器写法跨平台一致,多选行为一致 |
| 剪贴板 | clipboard | readText / writeText / readImage | 富文本格式各平台支持的标记集不同 |
| 通知 | Notification | isSupported / show | Windows 略过应用名与图标策略 |
| 全局快捷键 | globalShortcut | register / unregister | 注册全局生效,用完必须注销 |
文件对话框的正确姿势是"给出上下文":让对话框认得所属窗口(模态归属)、给出扩展名过滤器(少打字少出错)、区分打开与保存两个语义。常见错误是在渲染进程里找 dialog(找不到)或忘了传窗口引用导致对话框在 macOS 上悬浮无主。
// 主进程:一个规范的"选择图片"处理器 const { ipcMain, dialog } = require('electron'); ipcMain.handle('image:pick', async (event) => { const win = BrowserWindow.fromWebContents(event.sender); // 对话框归属窗口 const result = await dialog.showOpenDialog(win, { title: '选择要导入的图片', properties: ['openFile', 'multiSelections'], // 支持多选 filters: [ { name: '图片', extensions: ['png', 'jpg', 'jpeg', 'webp'] }, { name: '所有文件', extensions: ['*'] } ] }); if (result.canceled) return []; // 用户取消是正常路径 return result.filePaths; });
剪贴板是四项里唯一双向的(读和写都要)。写文本、写图片直白;读的时候要有"剪贴板内容不属于你的应用"的觉悟——用户可能复制了密码。工程惯例:只在用户显式动作(点了"粘贴"按钮)时读取,读完即用不留存,更不要做"剪贴板监听"这种被系统逐步收紧的行为。
通知的两条纪律:先问 isSupported 再发(个别环境不支持);频控是礼貌更是留存率问题——连发三条通知的应用会被用户在系统设置里一键静音。通知的点击事件回到主进程回调里,通常的动作是唤起对应窗口(3.3 节的 show + focus 组合)。
// 主进程:一条可点击的系统通知 const { Notification } = require('electron'); function notifyExportDone(win, count) { if (!Notification.isSupported()) return; // 环境兜底 const n = new Notification({ title: '导出完成', body: `共导出 ${count} 条记录,点击查看`, silent: false }); n.on('click', () => { win.show(); win.focus(); }); // 点击通知唤起窗口 n.show(); }
全局快捷键是四项里最"贵"的——它注册的是整个系统的按键,注册多了会跟其他软件打架,用户换了键盘布局还可能失灵。纪律有三:只注册用户显式配置的快捷键(别硬编码一堆);应用退出前必须全部注销(will-quit 钩子里干这件事);注册失败要有降级提示而不是静默吞掉。
// 全局快捷键的登记与注销 const { globalShortcut, app } = require('electron'); app.whenReady().then(() => { const ok = globalShortcut.register('CommandOrControl+Shift+N', () => { toggleQuickNoteWindow(); // 唤起快速笔记(见下方案例) }); if (!ok) console.warn('快捷键注册失败,可能被其他应用占用'); }); app.on('will-quit', () => { globalShortcut.unregisterAll(); // 不注销 = 污染系统的按键地图 });

背景:产品要在应用里加"随手记":全局快捷键唤出输入框,输入内容后自动存入当天笔记文件,同时写一份到剪贴板,重要内容弹通知确认。
操作:全局快捷键唤出一个无边框小窗(3.3 节的窗口模式);提交时页面经桥调用 quicknote:append 通道;主进程处理器里串起三项能力——文件按日期追加写入(userData 目录下按天分文件)、剪贴板写入正文、超过阈值时发通知。
// 主进程:quicknote 处理器,三项能力一次串联 const fs = require('node:fs/promises'); const path = require('node:path'); ipcMain.handle('quicknote:append', async (_e, text) => { if (typeof text !== 'string' || !text.trim()) { throw new Error('笔记内容不能为空'); // 参数校验:白名单不是免检通道 } const day = new Date().toISOString().slice(0, 10); const file = path.join(app.getPath('userData'), `notes-${day}.md`); await fs.appendFile(file, `- ${text}\n`, 'utf8'); // 能力一:文件 clipboard.writeText(text); // 能力二:剪贴板 const { size } = await fs.stat(file); if (size > 1024 * 64) { // 单日超 64KB 提醒归档 notifyExportDone(mainWin, '今天的速记已经不少了'); } return { file, size }; });
结果:用户在任何应用里按快捷键、打字、回车、回原应用粘贴——四步完成一次记录,全程不切换焦点。上线后这个功能成了日活最高的入口。
解读:闭环的价值在于用系统能力把交互路径压到最短,每项能力单独看都不稀奇,串起来才产生"桌面软件感"。变式:若要支持 Markdown 实时预览,把小窗升级为双栏(复用主窗口组件),系统能力层零改动——这正是"能力与界面分离"模式的好处。
第三章完工,窗口有了骨骼、流水线和四肢。但能力越多,被觊觎的面就越大——第四章给这栋房子上锁。