鸿蒙PC 使用 Electron 实现通知功能详解


问题背景

在桌面应用开发中,通知功能是一个重要的用户体验特性。无论是消息提醒、任务完成通知,还是系统状态变化提示,通知都能帮助用户及时了解应用的状态变化。

需求分析

  1. 显示系统通知:在系统通知中心显示应用通知

  2. 跨平台兼容:在 Windows、macOS、Linux 以及鸿蒙PC平台上都能正常工作

  3. 通知交互:支持通知点击、关闭等事件处理

  4. 权限管理:正确处理不同平台的通知权限

  5. 用户体验优化:提供友好的通知内容和样式

技术挑战

  1. 平台差异:不同操作系统的通知 API 和行为不同

  2. 权限处理:macOS、Windows、Linux 的通知权限机制不同

  3. API 兼容性:HTML5 Notification API 和 Electron Notification API 的使用差异

  4. 鸿蒙平台适配:需要确保在鸿蒙PC平台上也能正常工作

 

效果预览

image-20251121091636714

 


实现方案

方案对比

方案 优点 缺点 适用场景
HTML5 Notification API 简单直接,浏览器原生支持,跨平台兼容性好 需要用户授权,某些浏览器可能不支持 优先使用,最可靠
Electron Notification API 功能强大,可配置选项多,在主进程中运行 需要 IPC 通信,平台差异较大 降级方案,功能更丰富
第三方通知库 功能完整,统一接口 增加依赖,可能过度设计 复杂场景

最终方案

采用双重保障方案,确保在各种环境下都能正常工作:

  1. 优先使用 HTML5 Notification APInew Notification() - 浏览器原生 API,在鸿蒙PC平台上完全支持

  2. 降级到 Electron IPC + Notification API:通过主进程使用 Electron 的 Notification 模块

  3. 平台特定优化:针对不同平台使用不同的通知选项和配置

  4. 事件监听机制:监听通知点击、关闭、显示等事件

方案优势

  • 跨平台兼容:支持 Windows、macOS、Linux 和鸿蒙PC平台

  • 双重保障:HTML5 API 优先,Electron API 降级

  • 平台优化:针对不同平台使用最佳实践

  • 用户体验好:提供丰富的通知选项和交互功能


代码实现

1. 主进程实现(main.js)

在主进程中实现 Electron 通知功能:

const { app, BrowserWindow, ipcMain, Notification } = require('electron');
const path = require('path');
​
// 存储通知实例,用于后续关闭和管理
const notifications = new Map();
​
// 显示通知
ipcMain.handle('show-notification', async (event, options) => {
 console.log('收到显示通知请求:', options);
 
 // 检查通知权限
 if (!Notification.isSupported()) {
   console.warn('当前系统不支持通知功能');
   return { success: false, error: 'Notifications not supported' };
}
​
 try {
   // macOS 特殊处理:检查系统通知权限
   if (process.platform === 'darwin') {
     // macOS 上 Electron 的 Notification 不需要额外权限请求
     // 但需要确保应用在 Dock 中(通常 Electron 应用会自动处理)
     console.log('macOS 平台,准备显示通知');
  }
​
   // 创建通知选项(根据平台调整)
   const notificationOptions = {
     title: options.title || '通知',
     body: options.body || options.message || '',
     silent: options.silent || false
  };
​
   // macOS 特殊选项
   if (process.platform === 'darwin') {
     // macOS 支持 subtitle
     if (options.subtitle) {
       notificationOptions.subtitle = options.subtitle;
    }
     // macOS 支持 sound(使用系统默认声音)
     if (options.sound !== false) {
       notificationOptions.sound = options.sound || 'default';
    }
     // macOS 支持 urgency(但可能被系统忽略)
     if (options.urgency) {
       notificationOptions.urgency = options.urgency;
    }
  } else {
     // Linux/Windows/鸿蒙PC 选项
     notificationOptions.icon = options.icon || undefined;
     notificationOptions.urgency = options.urgency || 'normal';
     notificationOptions.timeoutType = options.timeoutType || 'default';
     if (options.actions && options.actions.length > 0) {
       notificationOptions.actions = options.actions;
    }
     if (options.closeButtonText) {
       notificationOptions.closeButtonText = options.closeButtonText;
    }
     if (options.hasReply) {
       notificationOptions.hasReply = options.hasReply;
       notificationOptions.replyPlaceholder = options.replyPlaceholder;
    }
  }
​
   console.log('创建通知,选项:', JSON.stringify(notificationOptions, null, 2));
​
   // 创建通知
   let notification;
   try {
     notification = new Notification(notificationOptions);
     console.log('通知对象创建成功');
  } catch (error) {
     console.error('创建通知对象失败:', error);
     throw error;
  }
​
   // 生成通知ID
   const notificationId = options.notificationId || Date.now();
​
   // 监听通知点击事件
   notification.on('click', () => {
     console.log('通知被点击:', notificationId);
     // 通知所有窗口
     BrowserWindow.getAllWindows().forEach(win => {
       if (!win.isDestroyed()) {
         win.webContents.send('notification-clicked', notificationId);
      }
    });
     
     // 如果指定了点击回调,激活窗口
     if (options.onClick !== false && mainWindow && !mainWindow.isDestroyed()) {
       mainWindow.show();
       mainWindow.focus();
    }
  });
​
   // 监听通知关闭事件
   notification.on('close', () => {
     console.log('通知已关闭:', notificationId);
     notifications.delete(notificationId);
  });
​
   // 监听通知显示事件
   notification.on('show', () => {
     console.log('✅ 通知已显示:', notificationId);
     console.log('通知标题:', notificationOptions.title);
     console.log('通知内容:', notificationOptions.body);
  });
   
   // 监听通知错误事件
   notification.on('error', (error) => {
     console.error('❌ 通知错误:', notificationId, error);
     console.error('错误详情:', error.message);
  });
​
   // 监听操作按钮点击事件(如果支持,非 macOS)
   if (process.platform !== 'darwin' && notificationOptions.actions && notificationOptions.actions.length > 0) {
     notification.on('action', (event, index) => {
       console.log('通知操作按钮被点击:', notificationId, index);
       BrowserWindow.getAllWindows().forEach(win => {
         if (!win.isDestroyed()) {
           win.webContents.send('notification-action-clicked', {
             notificationId: notificationId,
             actionIndex: index
          });
        }
      });
    });
  }
​
   // 所有平台都需要调用 show() 才能显示通知
   try {
     notification.show();
     console.log('通知 show() 调用成功');
     
     if (process.platform === 'darwin') {
       console.log('macOS 通知已调用 show(),应该会显示在通知中心');
    }
  } catch (error) {
     console.error('通知 show() 调用失败:', error);
     console.error('错误详情:', error.stack);
     if (process.platform !== 'darwin') {
       // 非 macOS 平台,show() 失败是严重错误
       throw error;
    }
  }
​
   // 存储通知实例(防止被垃圾回收)
   notifications.set(notificationId, notification);
   
   // 添加超时清理(5分钟后自动清理)
   setTimeout(() => {
     if (notifications.has(notificationId)) {
       notifications.delete(notificationId);
       console.log('通知实例已清理:', notificationId);
    }
  }, 5 * 60 * 1000);
​
   console.log('通知处理完成,ID:', notificationId);
   return { success: true, notificationId: notificationId };
} catch (error) {
   console.error('显示通知失败:', error);
   return { success: false, error: error.message };
}
});
​
// 关闭通知
ipcMain.on('close-notification', (event, notificationId) => {
 console.log('收到关闭通知请求:', notificationId);
 const notification = notifications.get(notificationId);
 if (notification) {
   notification.close();
   notifications.delete(notificationId);
} else {
   console.warn('通知不存在:', notificationId);
}
});
​
// 应用准备就绪时检查通知支持
app.whenReady().then(() => {
 // 检查通知是否支持
 if (Notification.isSupported()) {
   console.log('通知功能已支持');
   console.log('平台:', process.platform);
   
   // macOS 特殊处理:确保应用在 Dock 中
   if (process.platform === 'darwin') {
     if (app.dock) {
       app.dock.show().catch(err => {
         console.log('Dock 显示(可选):', err.message);
      });
    } else {
       console.warn('app.dock 不可用,可能影响 macOS 通知显示');
    }
  }
} else {
   console.warn('通知功能不支持');
   console.warn('请检查系统通知设置');
}
 
 createWindow();
});

2. 预加载脚本(preload.js)

在预加载脚本中暴露安全的通知 API:

const { contextBridge, ipcRenderer } = require('electron');
​
contextBridge.exposeInMainWorld('electronAPI', {
   versions: {
       chrome: process.versions.chrome,
       node: process.versions.node,
       electron: process.versions.electron
  },
   // 显示通知
   showNotification: (options) => {
       return ipcRenderer.invoke('show-notification', options);
  },
   // 关闭通知
   closeNotification: (notificationId) => {
       ipcRenderer.send('close-notification', notificationId);
  },
   // 监听通知点击事件
   onNotificationClick: (callback) => {
       ipcRenderer.on('notification-clicked', (event, notificationId) => {
           callback(notificationId);
      });
  },
   // 移除通知点击监听
   removeNotificationClickListener: () => {
       ipcRenderer.removeAllListeners('notification-clicked');
  }
});

3. 渲染进程实现(index.html)

在页面中实现显示通知的功能:

<!DOCTYPE html>
<html>
<head>
   <meta charset="UTF-8">
   <title>通知示例</title>
</head>
<body>
   <button οnclick="showNotification()">🔔 显示通知</button>
​
   <script>
       // 显示通知
       async function showNotification() {
           console.log('点击显示通知按钮');
           
           try {
               // 方法1: 优先使用浏览器原生 Notification API
               if ('Notification' in window) {
                   // 请求权限
                   if (Notification.permission === 'default') {
                       const permission = await Notification.requestPermission();
                       if (permission !== 'granted') {
                           alert('通知权限被拒绝');
                           return;
                      }
                  }
                   
                   if (Notification.permission === 'granted') {
                       // 使用浏览器原生通知
                       const notification = new Notification('坚果通知', {
                           body: '这是来自坚果的通知',
                           icon: undefined,
                           badge: undefined,
                           tag: 'electron-notification',
                           requireInteraction: false,
                           silent: false
                      });
                       
                       notification.onclick = () => {
                           console.log('通知被点击');
                           window.focus();
                      };
                       
                       notification.onshow = () => {
                           console.log('通知已显示');
                      };
                       
                       notification.onclose = () => {
                           console.log('通知已关闭');
                      };
                       
                       notification.onerror = (error) => {
                           console.error('通知错误:', error);
                      };
                       
                       return;
                  }
              }
               
               // 方法2: 使用 Electron IPC 显示通知
               if (window.electronAPI && window.electronAPI.showNotification) {
                   console.log('使用 Electron IPC 显示通知');
                   
                   // 检测平台,使用不同的选项
                   const isMac = navigator.platform.toUpperCase().indexOf('MAC') >= 0;
                   const notificationOptions = {
                       title: '坚果通知',
                       body: '这是来自坚果的通知',
                       message: '这是来自坚果的通知',
                       notificationId: Date.now(),
                       silent: false,
                       onClick: true
                  };
                   
                   // macOS 特殊选项
                   if (isMac) {
                       notificationOptions.subtitle = '坚果派';
                       notificationOptions.sound = 'default';
                  } else {
                       notificationOptions.urgency = 'normal';
                  }
                   
                   console.log('通知选项:', notificationOptions);
                   
                   const result = await window.electronAPI.showNotification(notificationOptions);
                   
                   if (result.success) {
                       console.log('通知显示成功,ID:', result.notificationId);
                       
                       // 监听通知点击事件
                       if (window.electronAPI.onNotificationClick) {
                           window.electronAPI.onNotificationClick((notificationId) => {
                               console.log('通知被点击:', notificationId);
                               alert('通知 ID ' + notificationId + ' 被点击了!');
                          });
                      }
                  } else {
                       console.error('显示通知失败:', result.error);
                       alert('显示通知失败: ' + result.error);
                  }
              } else {
                   alert('通知功能不可用');
              }
          } catch (error) {
               console.error('显示通知失败:', error);
               alert('显示通知出错: ' + error.message);
          }
      }
​
       // 页面加载时检查通知权限
       window.addEventListener('DOMContentLoaded', () => {
           if ('Notification' in window) {
               console.log('浏览器支持通知功能');
               console.log('当前通知权限:', Notification.permission);
          } else {
               console.log('浏览器不支持通知功能');
          }
      });
   </script>
</body>
</html>

鸿蒙PC平台兼容性

兼容性说明

重要提示:本文档中实现的所有代码在鸿蒙PC平台上都可以正常使用!

1. HTML5 Notification API 支持

鸿蒙PC平台基于 Chromium 内核,完全支持 HTML5 Notification API:

// 在鸿蒙PC平台上,这段代码可以正常工作
if ('Notification' in window) {
   const permission = await Notification.requestPermission();
   if (permission === 'granted') {
       const notification = new Notification('标题', {
           body: '内容'
      });
  }
}

2. Electron Notification API 支持

Electron 的 Notification API 在鸿蒙PC平台上完全支持:

// 在鸿蒙PC平台上,Electron Notification API 可以正常工作
const { Notification } = require('electron');
const notification = new Notification({
   title: '标题',
   body: '内容'
});
notification.show();

3. IPC 通信支持

Electron 的 IPC 通信机制在鸿蒙PC平台上完全支持:

// 主进程和渲染进程之间的通信在鸿蒙PC上正常工作
ipcMain.handle('show-notification', async (event, options) => {
   // 处理逻辑
});
​
// 渲染进程调用
const result = await window.electronAPI.showNotification(options);

4. 平台特性支持

鸿蒙PC平台支持 Linux 风格的通知选项:

  • icon - 通知图标

  • urgency - 通知紧急程度

  • timeoutType - 超时类型

  • actions - 操作按钮(如果系统支持)

  • closeButtonText - 关闭按钮文本

测试验证

在鸿蒙PC平台上测试结果:

  • ✅ HTML5 Notification API 正常工作

  • ✅ Electron Notification API 正常工作

  • ✅ IPC 通信正常

  • ✅ 通知点击事件正常

  • ✅ 通知关闭事件正常

  • ✅ 通知显示事件正常


遇到的问题与解决方案

问题一:macOS 上通知不显示

现象:在 macOS 上点击通知按钮后,通知没有显示。

原因分析

  1. macOS 需要应用在 Dock 中才能显示通知

  2. 需要调用 notification.show() 方法

  3. 系统通知权限可能被禁用

解决方案

  1. 确保应用在 Dock 中

if (process.platform === 'darwin' && app.dock) {
   app.dock.show();
}
  1. 调用 show() 方法

// macOS 上也需要调用 show()
notification.show();
  1. 检查系统通知设置

    • 打开"系统设置" > "通知与专注模式"

    • 找到应用并确保通知权限已启用

问题二:通知权限被拒绝

现象:浏览器原生通知 API 返回权限被拒绝。

解决方案

  1. 请求权限

if (Notification.permission === 'default') {
    const permission = await Notification.requestPermission();
    if (permission !== 'granted') {
        // 处理权限被拒绝的情况
        alert('通知权限被拒绝,请在浏览器设置中启用');
        return;
    }
}
  1. 降级到 Electron API

// 如果浏览器 API 不可用,使用 Electron API
if (window.electronAPI && window.electronAPI.showNotification) {
    await window.electronAPI.showNotification(options);
}

问题三:通知实例被垃圾回收

现象:通知显示后立即消失或被系统清理。

解决方案

  1. 存储通知实例

const notifications = new Map();
notifications.set(notificationId, notification);
  1. 防止过早清理

// 添加超时清理(5分钟后自动清理)
setTimeout(() => {
    if (notifications.has(notificationId)) {
        notifications.delete(notificationId);
    }
}, 5 * 60 * 1000);

问题四:不同平台的通知选项不同

现象:某些通知选项在某些平台上不支持。

解决方案

  1. 平台检测

if (process.platform === 'darwin') {
    // macOS 选项
    notificationOptions.subtitle = options.subtitle;
    notificationOptions.sound = options.sound;
} else {
    // Linux/Windows/鸿蒙PC 选项
    notificationOptions.icon = options.icon;
    notificationOptions.urgency = options.urgency;
}
  1. 条件添加选项

// 只在支持的平台上添加选项
if (options.subtitle && process.platform === 'darwin') {
    notificationOptions.subtitle = options.subtitle;
}

功能特性

1. 双重保障机制

  • 优先使用 HTML5 Notification API:简单、可靠、跨平台兼容

  • 降级到 Electron IPC:当 HTML5 API 不可用时,使用 Electron API

2. 平台特定优化

  • macOS:支持 subtitlesoundurgency 选项

  • Windows/Linux/鸿蒙PC:支持 iconurgencyactions 等选项

3. 事件监听

  • 点击事件:监听通知点击,可以激活应用窗口

  • 关闭事件:监听通知关闭,清理通知实例

  • 显示事件:监听通知显示,确认通知已成功显示

  • 错误事件:监听通知错误,处理异常情况

4. 通知管理

  • 通知ID:为每个通知分配唯一ID

  • 实例存储:存储通知实例,防止被垃圾回收

  • 自动清理:5分钟后自动清理通知实例


最佳实践

1. 权限处理

async function requestNotificationPermission() {
    if ('Notification' in window) {
        if (Notification.permission === 'default') {
            const permission = await Notification.requestPermission();
            return permission === 'granted';
        }
        return Notification.permission === 'granted';
    }
    return false;
}

2. 错误处理

try {
    const result = await window.electronAPI.showNotification(options);
    if (result.success) {
        console.log('通知显示成功');
    } else {
        console.error('通知显示失败:', result.error);
        // 显示友好的错误提示
        showError('通知发送失败,请检查系统通知设置');
    }
} catch (error) {
    console.error('通知错误:', error);
    // 降级处理
    showFallbackNotification();
}

3. 通知内容优化

const notificationOptions = {
    title: '简短明确的标题',  // 不超过50个字符
    body: '清晰的通知内容',   // 不超过200个字符
    // macOS
    subtitle: '副标题(可选)',
    // 其他平台
    icon: '/path/to/icon.png'  // 使用合适的图标
};

4. 通知去重

// 使用 tag 属性避免重复通知
const notification = new Notification('标题', {
    body: '内容',
    tag: 'unique-notification-tag'  // 相同 tag 的通知会被替换
});

5. 通知分组

// 使用 group 属性分组通知(如果支持)
const notificationOptions = {
    title: '标题',
    body: '内容',
    group: 'notification-group-name'
};

完整示例代码

main.js(完整版)

const { app, BrowserWindow, ipcMain, Notification } = require('electron');
const path = require('path');

let mainWindow;
const notifications = new Map();

function createWindow() {
  mainWindow = new BrowserWindow({
    width: 800,
    height: 600,
    webPreferences: {
      nodeIntegration: false,
      contextIsolation: true,
      preload: path.join(__dirname, 'preload.js'),
    },
  });

  mainWindow.loadFile('index.html');
  return mainWindow;
}

// 显示通知
ipcMain.handle('show-notification', async (event, options) => {
  if (!Notification.isSupported()) {
    return { success: false, error: 'Notifications not supported' };
  }

  try {
    const notificationOptions = {
      title: options.title || '通知',
      body: options.body || options.message || '',
      silent: options.silent || false
    };

    // 平台特定选项
    if (process.platform === 'darwin') {
      if (options.subtitle) notificationOptions.subtitle = options.subtitle;
      if (options.sound !== false) notificationOptions.sound = options.sound || 'default';
    } else {
      notificationOptions.icon = options.icon;
      notificationOptions.urgency = options.urgency || 'normal';
    }

    const notification = new Notification(notificationOptions);
    const notificationId = options.notificationId || Date.now();

    notification.on('click', () => {
      BrowserWindow.getAllWindows().forEach(win => {
        if (!win.isDestroyed()) {
          win.webContents.send('notification-clicked', notificationId);
        }
      });
      if (options.onClick !== false && mainWindow) {
        mainWindow.show();
        mainWindow.focus();
      }
    });

    notification.on('close', () => {
      notifications.delete(notificationId);
    });

    notification.show();
    notifications.set(notificationId, notification);

    setTimeout(() => {
      if (notifications.has(notificationId)) {
        notifications.delete(notificationId);
      }
    }, 5 * 60 * 1000);

    return { success: true, notificationId: notificationId };
  } catch (error) {
    return { success: false, error: error.message };
  }
});

// 关闭通知
ipcMain.on('close-notification', (event, notificationId) => {
  const notification = notifications.get(notificationId);
  if (notification) {
    notification.close();
    notifications.delete(notificationId);
  }
});

app.whenReady().then(() => {
  if (Notification.isSupported()) {
    console.log('通知功能已支持');
    if (process.platform === 'darwin' && app.dock) {
      app.dock.show();
    }
  }
  createWindow();
});

app.on('window-all-closed', () => {
  if (process.platform !== 'darwin') {
    app.quit();
  }
});

preload.js(完整版)

const { contextBridge, ipcRenderer } = require('electron');

contextBridge.exposeInMainWorld('electronAPI', {
    showNotification: (options) => {
        return ipcRenderer.invoke('show-notification', options);
    },
    closeNotification: (notificationId) => {
        ipcRenderer.send('close-notification', notificationId);
    },
    onNotificationClick: (callback) => {
        ipcRenderer.on('notification-clicked', (event, notificationId) => {
            callback(notificationId);
        });
    },
    removeNotificationClickListener: () => {
        ipcRenderer.removeAllListeners('notification-clicked');
    }
});

总结

实现要点

  1. 双重保障机制:优先使用 HTML5 API,降级到 Electron API

  2. 跨平台支持:支持 Windows、macOS、Linux 和鸿蒙PC平台

  3. 平台优化:针对不同平台使用最佳实践

  4. 事件监听:完善的错误处理和事件监听

关键优势

  • 鸿蒙PC平台完全兼容:所有代码在鸿蒙PC平台上都可以正常工作

  • HTML5 API 优先:在支持的平台上使用最可靠的方案

  • Electron API 降级:确保在不支持 HTML5 API 的环境下也能工作

  • 平台优化:针对不同平台使用最佳配置

注意事项

  1. 权限要求:某些平台需要用户授予通知权限

  2. 平台差异:不同平台的通知样式和行为可能不同

  3. 错误处理:完善的错误处理确保应用稳定性

  4. 实例管理:合理管理通知实例,防止内存泄漏

扩展方向

  1. 通知持久化:保存通知历史记录

  2. 通知分组:支持通知分组和折叠

  3. 自定义样式:支持自定义通知样式和图标

  4. 通知操作:支持更多操作按钮和交互


参考资料


作者:GitCode & 坚果派 Electron 鸿蒙适配团队

 

 

Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐