9.4 通知 API

9.4 通知 API

通知API(Notifications API)允许Web应用向用户显示系统级通知,即使浏览器窗口处于非活动状态也能提醒用户重要信息。本节将详细介绍通知API的使用方法、权限管理和高级功能实现。

基础使用与权限管理

1. 权限请求与检查

// 检查浏览器支持情况
if (!('Notification' in window)) {
  console.warn('当前浏览器不支持通知功能');
  return;
}

// 检查当前权限状态
async function checkNotificationPermission() {
  const status = Notification.permission;
  
  if (status === 'granted') {
    return true;
  } else if (status === 'denied') {
    console.warn('用户已拒绝通知权限');
    return false;
  } else {
    // 需要请求权限
    const permission = await Notification.requestPermission();
    return permission === 'granted';
  }
}

// 用户交互触发权限请求
document.getElementById('enable-notifications').addEventListener('click', async () => {
  const hasPermission = await checkNotificationPermission();
  if (hasPermission) {
    showWelcomeNotification();
  }
});

// 显示欢迎通知
function showWelcomeNotification() {
  new Notification('欢迎使用我们的服务', {
    body: '感谢您启用通知,我们会及时提醒您重要信息。',
    icon: '/images/notification-icon.png',
    vibrate: [200, 100, 200] // 振动模式(兼容移动设备)
  });
}

2. 通知基本配置

配置项 类型 描述
body String 通知正文内容
icon String 通知图标URL
image String 通知大图URL(部分浏览器支持)
badge String 应用标识图标URL
vibrate Array 振动模式数组(移动设备)
sound String 声音文件URL
dir String 文字方向(auto/ltr/rtl)
lang String 通知语言
tag String 通知标识,相同tag会替换之前通知
renotify Boolean 是否在替换通知时提醒用户
requireInteraction Boolean 是否保持通知直到用户交互(默认自动关闭)

高级通知功能

1. 服务工作者与推送通知

// 在Service Worker中处理推送事件
self.addEventListener('push', (event) => {
  const data = event.data.json();
  
  const options = {
    body: data.body,
    icon: data.icon || '/icons/icon-192x192.png',
    badge: '/icons/badge-72x72.png',
    data: { // 自定义数据
      url: data.url 
    },
    actions: [
      { action: 'open', title: '打开应用' },
      { action: 'close', title: '关闭' }
    ]
  };
  
  event.waitUntil(
    self.registration.showNotification(data.title, options)
  );
});

// 处理通知点击
self.addEventListener('notificationclick', (event) => {
  event.notification.close();
  
  if (event.action === 'open') {
    // 打开特定URL
    clients.openWindow(event.notification.data.url);
  } else {
    // 默认打开应用主页
    clients.openWindow('/');
  }
});

// 处理通知关闭
self.addEventListener('notificationclose', (event) => {
  // 可以发送分析数据
  sendAnalytics('notification_closed', {
    tag: event.notification.tag
  });
});

2. 交互式通知与操作按钮

function showActionNotification() {
  const notification = new Notification('新消息到达', {
    body: '您有3条未读消息,点击查看详情',
    icon: '/images/message-icon.png',
    actions: [
      { action: 'reply', title: '回复', icon: '/images/reply-icon.png' },
      { action: 'mark-read', title: '标记已读' }
    ],
    data: {
      messageId: 12345
    }
  });
  
  notification.onclick = (event) => {
    window.focus();
    navigateTo('/messages');
  };
  
  notification.onactionclick = (event) => {
    switch(event.action) {
      case 'reply':
        openReplyWindow(event.notification.data.messageId);
        break;
      case 'mark-read':
        markAsRead(event.notification.data.messageId);
        break;
    }
  };
}

最佳实践与优化

1. 通知策略管理

class NotificationManager {
  constructor() {
    this.queue = [];
    this.currentNotification = null;
    this.permission = Notification.permission;
  }
  
  async requestPermission() {
    if (this.permission !== 'default') return;
    
    this.permission = await Notification.requestPermission();
    if (this.permission === 'granted') {
      this.processQueue();
    }
  }
  
  show(title, options) {
    if (this.permission !== 'granted') {
      this.queue.push({ title, options });
      return;
    }
    
    if (this.currentNotification) {
      this.currentNotification.close();
    }
    
    this.currentNotification = new Notification(title, options);
    this.currentNotification.onclose = () => {
      this.currentNotification = null;
      this.processQueue();
    };
  }
  
  processQueue() {
    if (this.queue.length > 0 && !this.currentNotification) {
      const next = this.queue.shift();
      this.show(next.title, next.options);
    }
  }
  
  schedule(title, options, time) {
    const now = Date.now();
    const delay = time.getTime() - now;
    
    if (delay <= 0) {
      this.show(title, options);
      return;
    }
    
    setTimeout(() => {
      this.show(title, options);
    }, delay);
  }
}

// 使用示例
const notifier = new NotificationManager();
notifier.show('系统提醒', {
  body: '您的订阅即将到期',
  icon: '/icons/alert.png'
});

// 定时通知
const reminderTime = new Date(Date.now() + 3600000); // 1小时后
notifier.schedule('会议提醒', {
  body: '一小时后有产品会议',
  requireInteraction: true
}, reminderTime);

2. 用户偏好设置

// 存储用户通知偏好
class NotificationPreferences {
  constructor() {
    this.prefs = JSON.parse(localStorage.getItem('notificationPrefs')) || {
      messages: true,
      reminders: false,
      promotions: false
    };
  }
  
  update(key, value) {
    this.prefs[key] = value;
    localStorage.setItem('notificationPrefs', JSON.stringify(this.prefs));
  }
  
  canSend(type) {
    return this.permissionGranted() && this.prefs[type];
  }
  
  permissionGranted() {
    return Notification.permission === 'granted';
  }
  
  async requestPermission() {
    if (this.permissionGranted()) return true;
    
    const permission = await Notification.requestPermission();
    return permission === 'granted';
  }
}

// 使用示例
const prefs = new NotificationPreferences();

document.getElementById('message-notify').addEventListener('change', (e) => {
  prefs.update('messages', e.target.checked);
});

// 发送通知前检查
if (prefs.canSend('messages')) {
  new Notification('您有新消息', {
    body: '点击查看详细内容'
  });
}

跨浏览器兼容方案

1. 特性检测与降级

function showCompatibleNotification(title, options) {
  // 检查支持情况
  if (!('Notification' in window)) {
    // 降级方案:使用HTML通知
    return showHtmlNotification(title, options);
  }
  
  // 检查权限
  if (Notification.permission === 'granted') {
    return new Notification(title, options);
  }
  
  if (Notification.permission !== 'denied') {
    Notification.requestPermission().then(permission => {
      if (permission === 'granted') {
        new Notification(title, options);
      }
    });
  }
}

function showHtmlNotification(title, options) {
  const div = document.createElement('div');
  div.className = 'html-notification';
  
  if (options.icon) {
    const img = document.createElement('img');
    img.src = options.icon;
    div.appendChild(img);
  }
  
  const h3 = document.createElement('h3');
  h3.textContent = title;
  div.appendChild(h3);
  
  if (options.body) {
    const p = document.createElement('p');
    p.textContent = options.body;
    div.appendChild(p);
  }
  
  document.body.appendChild(div);
  
  // 自动消失
  setTimeout(() => {
    div.classList.add('fade-out');
    setTimeout(() => div.remove(), 300);
  }, 5000);
  
  return {
    close: () => div.remove()
  };
}

2. 移动端适配处理

// 检测移动设备
function isMobile() {
  return /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent);
}

// 移动端通知适配
function showAdaptiveNotification(title, options) {
  if (isMobile()) {
    // 移动端使用更简单的通知
    const simplifiedOptions = {
      body: options.body,
      icon: options.icon,
      vibrate: options.vibrate || [200, 100, 200]
    };
    
    // 避免在移动端使用requireInteraction
    if (simplifiedOptions.requireInteraction) {
      delete simplifiedOptions.requireInteraction;
    }
    
    return new Notification(title, simplifiedOptions);
  }
  
  // 桌面端使用完整功能
  return new Notification(title, options);
}

安全与隐私考量

  1. 权限滥用防护

    • 仅在用户明确操作后请求权限
    • 解释通知用途的价值主张
    function showPermissionRequest() {
      const dialog = document.getElementById('notification-permission-dialog');
      dialog.style.display = 'block';
      
      document.getElementById('enable-notifications').addEventListener('click', async () => {
        const granted = await checkNotificationPermission();
        if (granted) {
          dialog.style.display = 'none';
          showWelcomeNotification();
        }
      });
    }
    
  2. 内容安全策略

    • 限制通知中的外部资源
    • 验证数据来源
    function sanitizeNotification(data) {
      const allowedDomains = ['trusted-cdn.com', 'ourdomain.com'];
      const iconUrl = new URL(data.icon, location.href);
      
      if (!allowedDomains.includes(iconUrl.hostname)) {
        data.icon = '/default-notification-icon.png';
      }
      
      // 清理HTML内容
      if (data.body) {
        data.body = data.body.replace(/<[^>]*>/g, '');
      }
      
      return data;
    }
    
  3. 频率限制

    class RateLimitedNotifier {
      constructor(limit = 3, period = 60000) { // 每分钟最多3条
        this.limit = limit;
        this.period = period;
        this.timestamps = [];
      }
      
      canSend() {
        this.cleanup();
        return this.timestamps.length < this.limit;
      }
      
      recordSend() {
        this.timestamps.push(Date.now());
      }
      
      cleanup() {
        const now = Date.now();
        this.timestamps = this.timestamps.filter(t => now - t < this.period);
      }
      
      show(title, options) {
        if (!this.canSend()) {
          console.warn('通知频率限制');
          return null;
        }
        
        this.recordSend();
        return new Notification(title, options);
      }
    }
    
    // 使用示例
    const notifier = new RateLimitedNotifier();
    document.getElementById('send-alert').addEventListener('click', () => {
      notifier.show('重要提醒', { body: '请立即处理此问题' });
    });
    

实际应用案例

1. 聊天应用消息通知

class ChatNotifier {
  constructor() {
    this.currentChats = new Map(); // 保存各聊天通知引用
    this.permission = Notification.permission;
    this.setup();
  }
  
  async setup() {
    if (this.permission === 'default') {
      this.permission = await Notification.requestPermission();
    }
    
    // 监听标签页可见性变化
    document.addEventListener('visibilitychange', () => {
      if (document.hidden) {
        this.enabled = true;
      } else {
        // 当用户回到页面时关闭所有相关通知
        this.clearAll();
      }
    });
  }
  
  notifyNewMessage(chatId, userName, message) {
    if (!this.enabled || this.permission !== 'granted') return;
    
    // 如果已有该聊天的通知,先关闭
    if (this.currentChats.has(chatId)) {
      this.currentChats.get(chatId).close();
    }
    
    const notification = new Notification(`${userName}发来消息`, {
      body: message.text.length > 50 
        ? `${message.text.substring(0, 50)}...` 
        : message.text,
      icon: user.avatar || '/images/default-avatar.png',
      tag: `chat_${chatId}`,
      data: { chatId }
    });
    
    notification.onclick = () => {
      window.focus();
      navigateToChat(chatId);
      notification.close();
    };
    
    this.currentChats.set(chatId, notification);
  }
  
  clearAll() {
    this.currentChats.forEach(notification => notification.close());
    this.currentChats.clear();
  }
}

// 使用示例
const chatNotifier = new ChatNotifier();

// 收到新消息时
socket.on('new-message', (data) => {
  if (document.hidden || !document.hasFocus()) {
    chatNotifier.notifyNewMessage(data.chatId, data.userName, data.message);
  }
});

2. 任务提醒系统

class TaskReminder {
  constructor() {
    this.scheduled = new Map();
    this.checkPermission();
  }
  
  async checkPermission() {
    if (Notification.permission !== 'granted') {
      await Notification.requestPermission();
    }
  }
  
  scheduleReminder(task) {
    if (this.scheduled.has(task.id)) {
      clearTimeout(this.scheduled.get(task.id));
    }
    
    const now = Date.now();
    const delay = task.dueDate - now;
    
    if (delay <= 0) {
      this.showReminder(task);
      return;
    }
    
    const timer = setTimeout(() => {
      this.showReminder(task);
      this.scheduled.delete(task.id);
    }, delay);
    
    this.scheduled.set(task.id, timer);
  }
  
  showReminder(task) {
    if (Notification.permission !== 'granted') return;
    
    const notification = new Notification(`任务提醒: ${task.title}`, {
      body: task.description || '该任务已到期',
      icon: '/icons/task-reminder.png',
      requireInteraction: true,
      data: { taskId: task.id }
    });
    
    notification.onclick = () => {
      window.focus();
      openTaskEditor(task.id);
      notification.close();
    };
  }
  
  cancelReminder(taskId) {
    if (this.scheduled.has(taskId)) {
      clearTimeout(this.scheduled.get(taskId));
      this.scheduled.delete(taskId);
    }
  }
}

// 使用示例
const reminder = new TaskReminder();

// 添加新任务时
function addNewTask(task) {
  db.saveTask(task);
  reminder.scheduleReminder(task);
}

// 删除任务时
function deleteTask(taskId) {
  reminder.cancelReminder(taskId);
  db.deleteTask(taskId);
}

通知API为Web应用提供了强大的用户提醒能力,开发时应注意:

  • 尊重用户选择,合理请求权限
  • 提供清晰的价值主张说明通知用途
  • 实现频率限制避免滥用
  • 为不同平台提供适配体验
  • 确保通知内容安全无害

通过合理使用通知API,可以显著提升Web应用的用户参与度和留存率,特别是在PWA(渐进式Web应用)中,通知功能是实现原生体验的关键特性之一。

#前端开发 分享于 2025-05-20

【 内容由 AI 共享,不代表本站观点,请谨慎参考 】