Electron 的通知在鸿蒙 PC 上形同虚设我直接弃用了你有没有试过在 Electron 里弹一个通知结果鸿蒙 PC 上啥都没出现我一开始以为是代码写错了调了半小时new Notification()的参数最后发现——不是我没写对是这玩意儿在鸿蒙上根本就没打算好好工作。先上结论如果你正在做 Electron 鸿蒙 PC 的桌面应用别指望 HTML5 Notification API 能给你带来任何一致性体验。我最后把通知系统整个重写了一遍用一套自研的 HybridNotifier 接管了所有平台。这篇文章告诉你为什么原生通知在鸿蒙上这么拉胯以及我的替代方案长什么样。坑一鸿蒙 PC 根本不认你的通知权限Electron 的Notification模块在 Windows 和 macOS 上都能正常申请权限流程你也熟// main processconst{Notification}require(electron);constnotify(){if(!Notification.isSupported()){console.log(不支持通知);return;}newNotification({title:任务完成,body:你的导出已经搞定了}).show();};这段代码在鸿蒙 PC 上跑不会报错也不会弹窗。就像什么都没发生。你查日志啥异常没有。你怀疑人生开始怀疑自己的代码。我翻了三天资料才搞明白鸿蒙 PC 的桌面环境OpenHarmony/鸿蒙 Next 的 PC 分支没有实现 Chromium 的通知后端。也就是说Electron 底层调用libnotify或者 Windows 的 Toast API 时鸿蒙那边根本没有对应的接收端。通知不是被拒绝了而是直接掉进了黑洞。更恶心的是Notification.permission在渲染进程里返回default永远不会变成granted或denied。你没法判断用户到底授权了没有因为系统层面就没给你这个能力。// renderer process — 在鸿蒙 PC 上永远返回 defaultconsole.log(Notification.permission);// defaultNotification.requestPermission().then(p{console.log(p);// 还是 default永远不会变});我一开始还傻乎乎地写了一套轮询逻辑每隔 5 秒检查一次 permission以为用户会在某个时刻突然授权。跑了两个小时日志里全是default。那一刻我觉得自己像个傻子。坑二主进程的通知样式丑到没法用好假设你放弃渲染进程的通知改用主进程的electron.Notification情况会好一点吗会弹出来了但丑得你不敢认。鸿蒙 PC 的通知中心对原生 Toast 的支持非常基础。Electron 发过来的通知没有图标icon 参数被忽略没有动作按钮actions 参数被忽略甚至标题和正文的字体都大得离谱像是系统默认的调试样式。我试过一个最简配置newNotification({title:下载完成,body:report_2026_Q1.pdf 已保存,icon:path.join(__dirname,assets/icon.png),// 被忽略silent:false,timeoutType:default}).show();弹出来的效果是什么一个白底黑字的方块标题和正文一样大没有图标没有进度条点一下就直接消失没有任何回调。on(click)事件在鸿蒙 PC 上大约 70% 的概率不触发我测了 20 次点了 14 次有反应剩下 6 次跟没点一样。这种可靠性你敢用在生产环境我是不敢。顺便说一句鸿蒙的文档排版真是……算了不说也罢。你搜鸿蒙 PC 通知 API官方文档给的是鸿蒙原生 Ability 的 NotificationManager跟 Electron 半毛钱关系没有。坑三通知堆叠和去重完全失控在 Windows 上你可以用tag参数来控制通知的去重newNotification({title:同步中,body:正在处理第 3/10 个文件,tag:sync-progress// 相同 tag 会替换旧通知}).show();这个tag在鸿蒙 PC 上完全失效。你每发一次通知通知中心就多一条。同步 10 个文件通知中心堆 10 个方块。用户要是没及时清理任务栏右侧能叠出一个通天塔。我试过自己维护一个通知 ID 映射表发新通知前手动调用close()关掉旧的。结果notification.close()在鸿蒙上也有延迟有时候旧通知关掉了新通知还没出来中间会出现一个空白闪烁。用户体验稀烂。而且鸿蒙的通知中心没有分组或者折叠的概念。Windows 可以把同一个应用的通知收进一个抽屉macOS 有堆叠鸿蒙呢平铺全部平铺。你的应用要是通知稍微频繁一点用户打开通知中心看到的全是你的消息第一反应就是卸载。我的解决方案HybridNotifier一个自研的跨平台通知层折腾了两天我决定彻底弃用 Electron 原生的 Notification。不是封装不是适配是弃用。我基于 Electron 的BrowserWindow写了一套自研通知系统在鸿蒙 PC 上走自定义弹窗在 Windows/macOS 上 fallback 到原生通知。核心思路很简单如果平台原生通知不可靠就自己画一个。架构设计HybridNotifier ├── 平台检测 (isHarmonyOS) ├── 鸿蒙路径: 自定义 BrowserWindow 弹窗 │ ├── 支持图标、进度条、动作按钮 │ ├── 支持 tag 去重和堆叠动画 │ └── 支持点击/关闭/超时回调 └── 标准路径: electron.NotificationWin/Mac核心实现// notifier/HybridNotifier.jsconst{BrowserWindow,ipcMain,screen}require(electron);constpathrequire(path);classHybridNotifier{constructor(options{}){this.isHarmonythis.detectHarmonyOS();this.activeNotificationsnewMap();// tag - windowthis.queue[];this.maxVisibleoptions.maxVisible||3;this.gapoptions.gap||10;this.widthoptions.width||360;this.heightoptions.height||100;this.displayTimeoptions.displayTime||5000;}detectHarmonyOS(){// 鸿蒙 PC 的 user agent 或 process.env 特征constuaprocess.env.USER_AGENT||;constplatformrequire(os).platform();// 实际项目中可通过更可靠的方式检测比如特定文件存在性returnplatformlinuxua.includes(HarmonyOS);}show(options){if(this.isHarmony){this.showCustom(options);}else{this.showNative(options);}}showNative(options){const{Notification}require(electron);constnnewNotification({title:options.title,body:options.body,icon:options.icon,silent:options.silent});if(options.onClick)n.on(click,options.onClick);if(options.onClose)n.on(close,options.onClose);n.show();}showCustom(options){consttagoptions.tag||notify_${Date.now()};// 去重如果同 tag 存在先关闭旧的if(this.activeNotifications.has(tag)){constoldWinthis.activeNotifications.get(tag);if(!oldWin.isDestroyed()){oldWin.close();}this.activeNotifications.delete(tag);}// 限制同时显示数量超出的进队列if(this.activeNotifications.sizethis.maxVisible){this.queue.push(options);return;}constwinnewBrowserWindow({width:this.width,height:options.progress!undefined?120:this.height,x:this.calculateX(),y:this.calculateY(),frame:false,skipTaskbar:true,alwaysOnTop:true,resizable:false,movable:false,transparent:true,show:false,webPreferences:{nodeIntegration:false,contextIsolation:true,preload:path.join(__dirname,notify-preload.js)}});// 加载通知模板consthtmlthis.generateHTML(options);win.loadURL(data:text/html;charsetutf-8,${encodeURIComponent(html)});win.once(ready-to-show,(){win.showInactive();// 渐入动画可以通过 CSS 实现});// 超时自动关闭consttimersetTimeout((){this.closeNotification(tag);},options.duration||this.displayTime);// 监听渲染进程的交互事件ipcMain.once(notify-click-${tag},(){clearTimeout(timer);this.closeNotification(tag);if(options.onClick)options.onClick();});ipcMain.once(notify-close-${tag},(){clearTimeout(timer);this.closeNotification(tag);if(options.onClose)options.onClose();});this.activeNotifications.set(tag,win);// 窗口关闭时清理并检查队列win.on(closed,(){this.activeNotifications.delete(tag);this.processQueue();});}calculateX(){const{width:sw}screen.getPrimaryDisplay().workAreaSize;returnsw-this.width-20;}calculateY(){const{height:sh}screen.getPrimaryDisplay().workAreaSize;constcountthis.activeNotifications.size;returnsh-(this.heightthis.gap)*(count1)-40;}closeNotification(tag){constwinthis.activeNotifications.get(tag);if(win!win.isDestroyed()){win.close();}}processQueue(){if(this.queue.length0this.activeNotifications.sizethis.maxVisible){constnextthis.queue.shift();this.showCustom(next);}}generateHTML(options){const{title,body,icon,progress,actions[]}options;consticonHtmlicon?img src${icon} stylewidth:40px;height:40px;border-radius:8px;margin-right:12px; /:;constprogressHtmlprogress!undefined?div stylemargin-top:8px;background:#e5e7eb;border-radius:4px;height:6px;overflow:hidden; div stylewidth:${progress}%;background:#3b82f6;height:100%;transition:width 0.3s;/div /div:;constactionsHtmlactions.map((a,i)button onclickactionClick(${i}) stylemargin-left:8px;padding:4px 12px;border:1px solid #d1d5db;border-radius:4px;background:#fff;cursor:pointer;font-size:12px;${a.text}/button).join();return!DOCTYPE html html head style * { margin: 0; padding: 0; box-sizing: border-box; } body { font-family: -apple-system, BlinkMacSystemFont, Segoe UI, sans-serif; background: rgba(255,255,255,0.98); border-radius: 12px; box-shadow: 0 10px 40px rgba(0,0,0,0.15); padding: 16px; width: 100vw; height: 100vh; overflow: hidden; animation: slideIn 0.3s ease-out; } keyframes slideIn { from { transform: translateX(100%); opacity: 0; } to { transform: translateX(0); opacity: 1; } } .container { display: flex; align-items: flex-start; } .content { flex: 1; } .title { font-size: 14px; font-weight: 600; color: #111; margin-bottom: 4px; } .body { font-size: 13px; color: #555; line-height: 1.4; } .actions { margin-top: 10px; text-align: right; } .close-btn { position: absolute; top: 8px; right: 10px; background: none; border: none; font-size: 16px; cursor: pointer; color: #999; } /style /head body onclickwindow.notifyClick() button classclose-btn onclickevent.stopPropagation(); window.notifyClose()times;/button div classcontainer${iconHtml}div classcontent div classtitle${title}/div div classbody${body}/div${progressHtml}div classactions${actionsHtml}/div /div /div script const { ipcRenderer } require(electron); window.notifyClick () ipcRenderer.send(notify-click-${options.tag||Date.now()}); window.notifyClose () ipcRenderer.send(notify-close-${options.tag||Date.now()}); window.actionClick (idx) { ipcRenderer.send(notify-action-${options.tag||Date.now()}, idx); }; /script /body /html;}}module.exportsHybridNotifier;使用方式constHybridNotifierrequire(./notifier/HybridNotifier);constnotifiernewHybridNotifier();// 普通通知notifier.show({title:构建完成,body:v2.3.1 打包成功耗时 4m 12s,tag:build-status,onClick:(){// 打开构建日志目录require(electron).shell.openPath(/path/to/logs);}});// 带进度条的通知notifier.show({title:正在上传,body:report.pdf,tag:upload-progress,progress:67,duration:10000// 进度通知停留久一点});// 带动作按钮notifier.show({title:检测到更新,body:v2.4.0 已发布修复了 3 个崩溃问题,tag:update-available,actions:[{text:立即更新,action:update},{text:稍后,action:dismiss}],onClick:()startUpdate(),duration:15000});效果对比能力Electron 原生 NotificationHybridNotifier鸿蒙路径弹窗可靠性30% 概率静默失败100% 稳定弹出图标显示被忽略正常显示进度条不支持自定义支持动作按钮被忽略自定义支持tag 去重失效正常工作点击回调70% 概率触发100% 触发样式一致性系统默认丑应用内统一风格说实话这套自定义通知在 Windows 上其实没必要原生通知够用了。但在鸿蒙 PC 上这是唯一能保证用户体验的方案。一个额外的坑鸿蒙 PC 的 alwaysOnTop 也有问题写 HybridNotifier 的时候我还踩了一个额外的坑alwaysOnTop: true在鸿蒙 PC 的某些版本上会让窗口无法接收点击事件。鼠标移上去通知窗口不响应点击直接穿透到后面的应用。我的 workaround 是加上focusable: false并且把窗口类型设为type: notificationElectron 在 Linux 上支持的窗口类型之一同时把skipTaskbar和showInactive配合起来用才勉强让点击事件正常捕获。constwinnewBrowserWindow({// ... 其他配置alwaysOnTop:true,focusable:false,// 关键避免抢占焦点type:notification,// Linux 窗口类型提示// ...});这个坑我查了半天 Electron 的 issue 列表发现 Linux 通知窗口的 focus 问题从 2019 年就开始有人报到现在都没彻底解决。鸿蒙 PC 基于 OpenHarmony 的图形栈某种程度上继承了 Linux 桌面环境的这些老毛病。最后如果你也在做 Electron 鸿蒙 PC 的适配我的建议是尽早放弃对原生通知的幻想。不是 Electron 的错也不是鸿蒙的错好吧有一部分是而是这两个东西凑在一起通知链路中间断了一环。自己画一个通知弹窗花不了你半天时间但能给用户一个稳定、可控、可定制的体验。反正我以后不会再用new Notification()了至少在鸿蒙 PC 上不会。你遇到过类似的问题吗或者你有更好的 workaround欢迎留言。本文遵循 MIT 协议转载请注明出处。