当前位置:首页>鸿蒙APP>Electron 的通知在鸿蒙 PC 上形同虚设,我直接弃用了

Electron 的通知在鸿蒙 PC 上形同虚设,我直接弃用了

  • 2026-11-02 16:05:12
Electron 的通知在鸿蒙 PC 上形同虚设,我直接弃用了

Electron 的通知在鸿蒙 PC 上形同虚设,我直接弃用了

你有没有试过在 Electron 里弹一个通知,结果鸿蒙 PC 上啥都没出现?我一开始以为是代码写错了,调了半小时 new Notification() 的参数,最后发现——不是我没写对,是这玩意儿在鸿蒙上根本就没打算好好工作。

先上结论:如果你正在做 Electron + 鸿蒙 PC 的桌面应用,别指望 HTML5 Notification API 能给你带来任何一致性体验。 我最后把通知系统整个重写了一遍,用一套自研的 HybridNotifier 接管了所有平台。这篇文章告诉你为什么原生通知在鸿蒙上这么拉胯,以及我的替代方案长什么样。


坑一:鸿蒙 PC 根本不认你的通知权限

Electron 的 Notification 模块在 Windows 和 macOS 上都能正常申请权限,流程你也熟:

// main processconst { Notification } = require('electron');const notify = () => {  if (!Notification.isSupported()) {    console.log('不支持通知');    return;  }  new Notification({    title: '任务完成',    body: '你的导出已经搞定了'  }).show();};

这段代码在鸿蒙 PC 上跑,不会报错,也不会弹窗。 就像什么都没发生。你查日志,啥异常没有。你怀疑人生,开始怀疑自己的代码。

我翻了三天资料才搞明白:鸿蒙 PC 的桌面环境(OpenHarmony/鸿蒙 Next 的 PC 分支)没有实现 Chromium 的通知后端。 也就是说,Electron 底层调用 libnotify 或者 Windows 的 Toast API 时,鸿蒙那边根本没有对应的接收端。通知不是被拒绝了,而是直接掉进了黑洞。

更恶心的是,Notification.permission 在渲染进程里返回 "default",永远不会变成 "granted" 或 "denied"。你没法判断用户到底授权了没有,因为系统层面就没给你这个能力。

// renderer process — 在鸿蒙 PC 上永远返回 "default"console.log(Notification.permission); // "default"Notification.requestPermission().then(p => {  console.log(p); // 还是 "default",永远不会变});

我一开始还傻乎乎地写了一套轮询逻辑,每隔 5 秒检查一次 permission,以为用户会在某个时刻突然授权。跑了两个小时,日志里全是 "default"。那一刻我觉得自己像个傻子。


坑二:主进程的通知样式丑到没法用

好,假设你放弃渲染进程的通知,改用主进程的 electron.Notification,情况会好一点吗?

会弹出来了,但丑得你不敢认。

鸿蒙 PC 的通知中心对原生 Toast 的支持非常基础。Electron 发过来的通知没有图标(icon 参数被忽略),没有动作按钮(actions 参数被忽略),甚至标题和正文的字体都大得离谱,像是系统默认的调试样式。

我试过一个最简配置:

new Notification({  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 参数来控制通知的去重:

new Notification({  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.Notification(Win/Mac)

核心实现

// notifier/HybridNotifier.jsconst { BrowserWindow, ipcMain, screen } = require('electron');const path = require('path');class HybridNotifier {  constructor(options = {}) {    this.isHarmony = this.detectHarmonyOS();    this.activeNotifications = new Map(); // tag -> window    this.queue = [];    this.maxVisible = options.maxVisible || 3;    this.gap = options.gap || 10;    this.width = options.width || 360;    this.height = options.height || 100;    this.displayTime = options.displayTime || 5000;  }  detectHarmonyOS() {    // 鸿蒙 PC 的 user agent 或 process.env 特征    const ua = process.env.USER_AGENT || '';    const platform = require('os').platform();    // 实际项目中可通过更可靠的方式检测,比如特定文件存在性    return platform === 'linux' && ua.includes('HarmonyOS');  }  show(options) {    if (this.isHarmony) {      this.showCustom(options);    } else {      this.showNative(options);    }  }  showNative(options) {    const { Notification } = require('electron');    const n = new Notification({      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) {    const tag = options.tag || `notify_{encodeURIComponent(html)}`);    win.once('ready-to-show', () => {      win.showInactive();      // 渐入动画可以通过 CSS 实现    });    // 超时自动关闭    const timer = setTimeout(() => {      this.closeNotification(tag);    }, options.duration || this.displayTime);    // 监听渲染进程的交互事件    ipcMain.once(`notify-click-{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;    return sw - this.width - 20;  }  calculateY() {    const { height: sh } = screen.getPrimaryDisplay().workAreaSize;    const count = this.activeNotifications.size;    return sh - (this.height + this.gap) * (count + 1) - 40;  }  closeNotification(tag) {    const win = this.activeNotifications.get(tag);    if (win && !win.isDestroyed()) {      win.close();    }  }  processQueue() {    if (this.queue.length > 0 && this.activeNotifications.size < this.maxVisible) {      const next = this.queue.shift();      this.showCustom(next);    }  }  generateHTML(options) {    const { title, body, icon, progress, actions = [] } = options;    const iconHtml = icon ? `<img src="{progress}%;background:#3b82f6;height:100%;transition:width 0.3s;"></div>         </div>`      : '';    const actionsHtml = actions.map((a, i) =>      `<button onclick="actionClick({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 onclick="window.notifyClick()">  <button class="close-btn" onclick="event.stopPropagation(); window.notifyClose()">&times;</button>  <div class="container">{title}</div>      <div class="body">{progressHtml}      <div class="actions">{options.tag || Date.now()}');    window.notifyClose = () => ipcRenderer.send('notify-close-{options.tag || Date.now()}', idx);    };  </script></body></html>`;  }}module.exports = HybridNotifier;

使用方式

const HybridNotifier = require('./notifier/HybridNotifier');const notifier = new HybridNotifier();// 普通通知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 原生 Notification
HybridNotifier(鸿蒙路径)
弹窗可靠性
30% 概率静默失败
100% 稳定弹出
图标显示
被忽略
正常显示
进度条
不支持
自定义支持
动作按钮
被忽略
自定义支持
tag 去重
失效
正常工作
点击回调
70% 概率触发
100% 触发
样式一致性
系统默认,丑
应用内统一风格

说实话,这套自定义通知在 Windows 上其实没必要,原生通知够用了。但在鸿蒙 PC 上,这是唯一能保证用户体验的方案。


一个额外的坑:鸿蒙 PC 的 alwaysOnTop 也有问题

写 HybridNotifier 的时候我还踩了一个额外的坑:alwaysOnTop: true 在鸿蒙 PC 的某些版本上会让窗口无法接收点击事件。 鼠标移上去,通知窗口不响应,点击直接穿透到后面的应用。

我的 workaround 是加上 focusable: false 并且把窗口类型设为 type: 'notification'(Electron 在 Linux 上支持的窗口类型之一),同时把 skipTaskbar 和 showInactive 配合起来用,才勉强让点击事件正常捕获。

const win = new BrowserWindow({  // ... 其他配置  alwaysOnTop: true,  focusable: false, // 关键:避免抢占焦点  type: 'notification', // Linux 窗口类型提示  // ...});

这个坑我查了半天 Electron 的 issue 列表,发现 Linux 通知窗口的 focus 问题从 2019 年就开始有人报,到现在都没彻底解决。鸿蒙 PC 基于 OpenHarmony 的图形栈,某种程度上继承了 Linux 桌面环境的这些老毛病。


最后

如果你也在做 Electron + 鸿蒙 PC 的适配,我的建议是:尽早放弃对原生通知的幻想。 不是 Electron 的错,也不是鸿蒙的错(好吧,有一部分是),而是这两个东西凑在一起,通知链路中间断了一环。

自己画一个通知弹窗,花不了你半天时间,但能给用户一个稳定、可控、可定制的体验。反正我以后不会再用 new Notification() 了,至少在鸿蒙 PC 上不会。

你遇到过类似的问题吗?或者你有更好的 workaround?欢迎留言。

本文遵循 MIT 协议,转载请注明出处。

最新文章

随机文章