强曰为道

与天地相似,故不违。知周乎万物,而道济天下,故不过。旁行而不流,乐天知命,故不忧.
文档目录

第 13 章:通知系统(Notifications)

第 13 章:通知系统(Notifications)

通知系统让扩展能够及时向用户传递重要信息。Chrome 扩展支持桌面通知(Notifications)和工具栏徽章(Badge)两种形式,它们是扩展与用户保持联系的关键通道。


13.1 桌面通知

13.1.1 权限声明

{
  "permissions": ["notifications"]
}

13.1.2 创建通知

// 简单通知
chrome.notifications.create('notification-1', {
  type: 'basic',
  iconUrl: 'icons/icon-128.png',
  title: '任务完成',
  message: '数据同步已完成,共更新 42 条记录。',
  priority: 1
});

// 带按钮的通知
chrome.notifications.create('notification-2', {
  type: 'basic',
  iconUrl: 'icons/icon-128.png',
  title: '新消息',
  message: '您有一条来自团队的消息,请点击查看。',
  buttons: [
    { title: '查看消息' },
    { title: '稍后提醒' }
  ],
  priority: 2
});

// 图片通知
chrome.notifications.create('notification-3', {
  type: 'image',
  iconUrl: 'icons/icon-128.png',
  title: '截图已保存',
  message: '页面截图已保存到本地。',
  imageUrl: 'screenshot-preview.png',
  priority: 1
});

// 列表通知
chrome.notifications.create('notification-4', {
  type: 'list',
  iconUrl: 'icons/icon-128.png',
  title: '待办事项',
  message: '您有以下待办事项:',
  items: [
    { title: '完成报告', message: '截止日期:今天' },
    { title: '代码审查', message: 'PR #123 等待审批' },
    { title: '团队会议', message: '下午 3:00' }
  ],
  priority: 1
});

// 进度通知
chrome.notifications.create('notification-5', {
  type: 'progress',
  iconUrl: 'icons/icon-128.png',
  title: '下载中',
  message: '正在下载文件...',
  progress: 45,
  priority: 1
});

通知类型

type说明特有字段
basic基本文本通知
image带图片通知imageUrl
list列表通知items
progress进度条通知progress (0-100)

13.1.3 通知事件处理

// 通知被点击
chrome.notifications.onClicked.addListener((notificationId) => {
  console.log('通知被点击:', notificationId);

  switch (notificationId) {
    case 'new-message':
      chrome.tabs.create({ url: 'https://messages.example.com' });
      break;
    case 'update-available':
      chrome.tabs.create({ url: 'chrome://extensions/' });
      break;
  }

  // 关闭通知
  chrome.notifications.clear(notificationId);
});

// 通知按钮被点击
chrome.notifications.onButtonClicked.addListener(
  (notificationId, buttonIndex) => {
    console.log(`通知 ${notificationId} 的按钮 ${buttonIndex} 被点击`);

    switch (notificationId) {
      case 'new-message':
        if (buttonIndex === 0) {
          // "查看消息"
          chrome.tabs.create({ url: 'https://messages.example.com' });
        } else {
          // "稍后提醒"
          chrome.alarms.create('remindMessage', { delayInMinutes: 30 });
        }
        break;
    }

    chrome.notifications.clear(notificationId);
  }
);

// 通知被关闭
chrome.notifications.onClosed.addListener((notificationId, byUser) => {
  console.log(`通知 ${notificationId} 已关闭, 用户关闭: ${byUser}`);
});

13.2 通知管理

13.2.1 更新通知

// 更新进度通知
async function updateProgress(current, total) {
  const percent = Math.round((current / total) * 100);

  await chrome.notifications.update('download-progress', {
    message: `下载中... ${current}/${total} (${percent}%)`,
    progress: percent
  });
}

// 更新通知内容
async function updateNotification(id, updates) {
  try {
    await chrome.notifications.update(id, updates);
  } catch (error) {
    // 通知可能已被清除
    console.warn('更新通知失败:', error);
  }
}

13.2.2 通知限制

限制说明
同时显示数量最多约 3-4 个(系统相关)
无声音选项MV3 不支持 requireInteraction
更新频率过于频繁的更新可能被限制
图片尺寸imageUrl 推荐 128×128 以上

13.3 工具栏徽章(Badge)

徽章是显示在扩展图标上的小文本标签,非常适合显示未读数量或状态指示。

13.3.1 设置徽章文本

// 设置徽章文本(最多 4 个字符)
await chrome.action.setBadgeText({ text: '5' });

// 为特定标签页设置
await chrome.action.setBadgeText({
  text: '3',
  tabId: tabId
});

// 清除徽章
await chrome.action.setBadgeText({ text: '' });

13.3.2 设置徽章颜色

// 设置背景颜色
await chrome.action.setBadgeBackgroundColor({ color: '#4285f4' });

// 根据状态改变颜色
function setBadgeStatus(count) {
  if (count === 0) {
    chrome.action.setBadgeText({ text: '' });
  } else {
    chrome.action.setBadgeText({ text: String(count) });

    const color = count > 10 ? '#ea4335' :
                  count > 5  ? '#fbbc04' : '#34a853';
    chrome.action.setBadgeBackgroundColor({ color });
  }
}

13.3.3 动态图标

// 切换图标
async function setExtensionIcon(isActive) {
  await chrome.action.setIcon({
    path: isActive ? {
      16: 'icons/active-16.png',
      32: 'icons/active-32.png',
      48: 'icons/active-48.png',
      128: 'icons/active-128.png'
    } : {
      16: 'icons/icon-16.png',
      32: 'icons/icon-32.png',
      48: 'icons/icon-48.png',
      128: 'icons/icon-128.png'
    }
  });
}

// 使用 Canvas 动态生成图标
async function setDynamicIcon(text) {
  const canvas = new OffscreenCanvas(128, 128);
  const ctx = canvas.getContext('2d');

  // 绘制背景
  ctx.fillStyle = '#4285f4';
  ctx.beginPath();
  ctx.arc(64, 64, 60, 0, Math.PI * 2);
  ctx.fill();

  // 绘制文字
  ctx.fillStyle = 'white';
  ctx.font = 'bold 48px sans-serif';
  ctx.textAlign = 'center';
  ctx.textBaseline = 'middle';
  ctx.fillText(text, 64, 64);

  const imageData = ctx.getImageData(0, 0, 128, 128);
  await chrome.action.setIcon({ imageData });
}

13.3.4 设置提示文本

// 动态更新 tooltip
async function setTooltip(text) {
  await chrome.action.setTitle({ title: text });
}

// 根据上下文更新
chrome.tabs.onActivated.addListener(async (activeInfo) => {
  const tab = await chrome.tabs.get(activeInfo.tabId);
  await chrome.action.setTitle({
    title: `我的扩展 - ${tab.title}`,
    tabId: tab.id
  });
});

13.4 通知系统设计模式

13.4.1 消息队列通知

class NotificationQueue {
  constructor() {
    this.queue = [];
    this.isShowing = false;
  }

  async add(notification) {
    this.queue.push(notification);
    if (!this.isShowing) {
      await this.showNext();
    }
  }

  async showNext() {
    if (this.queue.length === 0) {
      this.isShowing = false;
      return;
    }

    this.isShowing = true;
    const notification = this.queue.shift();

    const id = `notif-${Date.now()}`;
    await chrome.notifications.create(id, {
      type: 'basic',
      iconUrl: 'icons/icon-128.png',
      ...notification
    });

    // 5 秒后自动显示下一个
    setTimeout(() => {
      chrome.notifications.clear(id);
      this.showNext();
    }, 5000);
  }
}

const notifQueue = new NotificationQueue();
notifQueue.add({ title: '通知 1', message: '第一条消息' });
notifQueue.add({ title: '通知 2', message: '第二条消息' });
notifQueue.add({ title: '通知 3', message: '第三条消息' });

13.4.2 通知偏好管理

class NotificationManager {
  constructor() {
    this.prefs = {
      enabled: true,
      sound: true,
      showBadge: true,
      quietHoursStart: '22:00',
      quietHoursEnd: '08:00',
      categories: {
        system: true,
        updates: true,
        social: false,
        marketing: false
      }
    };
  }

  async loadPrefs() {
    const { notificationPrefs } = await chrome.storage.sync.get('notificationPrefs');
    if (notificationPrefs) {
      Object.assign(this.prefs, notificationPrefs);
    }
  }

  async shouldNotify(category) {
    await this.loadPrefs();

    if (!this.prefs.enabled) return false;
    if (!this.prefs.categories[category]) return false;
    if (this.isQuietHours()) return false;

    return true;
  }

  isQuietHours() {
    const now = new Date();
    const hours = now.getHours();
    const minutes = now.getMinutes();
    const currentTime = `${String(hours).padStart(2, '0')}:${String(minutes).padStart(2, '0')}`;

    const start = this.prefs.quietHoursStart;
    const end = this.prefs.quietHoursEnd;

    if (start <= end) {
      return currentTime >= start && currentTime <= end;
    } else {
      // 跨午夜 (如 22:00 - 08:00)
      return currentTime >= start || currentTime <= end;
    }
  }

  async notify(category, options) {
    if (!(await this.shouldNotify(category))) return;

    const id = `notif-${Date.now()}`;
    await chrome.notifications.create(id, {
      type: 'basic',
      iconUrl: 'icons/icon-128.png',
      ...options
    });

    if (this.prefs.showBadge) {
      await this.incrementBadge();
    }

    return id;
  }

  async incrementBadge() {
    const { badgeCount = 0 } = await chrome.storage.session.get('badgeCount');
    const newCount = badgeCount + 1;
    await chrome.storage.session.set({ badgeCount: newCount });

    chrome.action.setBadgeText({ text: String(newCount) });
    chrome.action.setBadgeBackgroundColor({ color: '#ea4335' });
  }

  async clearBadge() {
    await chrome.storage.session.set({ badgeCount: 0 });
    chrome.action.setBadgeText({ text: '' });
  }
}

// 使用
const notifier = new NotificationManager();
await notifier.notify('updates', {
  title: '新版本可用',
  message: '扩展 v2.0 已发布,点击查看更新内容。'
});

13.5 业务场景

场景一:定时提醒

chrome.runtime.onInstalled.addListener(() => {
  chrome.alarms.create('checkEmail', { periodInMinutes: 15 });
});

chrome.alarms.onAlarm.addListener(async (alarm) => {
  if (alarm.name === 'checkEmail') {
    const { count } = await checkNewEmails();
    if (count > 0) {
      chrome.notifications.create('new-email', {
        type: 'basic',
        iconUrl: 'icons/icon-128.png',
        title: '新邮件',
        message: `您有 ${count} 封未读邮件`,
        buttons: [{ title: '查看邮箱' }],
        priority: 2
      });

      chrome.action.setBadgeText({ text: String(count) });
      chrome.action.setBadgeBackgroundColor({ color: '#ea4335' });
    }
  }
});

场景二:下载完成通知

chrome.downloads.onChanged.addListener((downloadDelta) => {
  if (downloadDelta.state?.current === 'complete') {
    chrome.notifications.create(`download-${downloadDelta.id}`, {
      type: 'basic',
      iconUrl: 'icons/icon-128.png',
      title: '下载完成',
      message: '文件已下载完成。',
      buttons: [{ title: '打开文件' }, { title: '打开文件夹' }]
    });
  }
});

13.6 注意事项

问题说明解决方案
通知被系统拦截用户关闭了 Chrome 通知引导用户开启系统通知
徽章不显示需要 action 配置检查 manifest 中的 action
按钮不显示macOS 限制macOS 通知中心可能不显示按钮
进度条不更新频率限制降低更新频率
requireInteraction 无效MV3 变更暂时无法解决

13.7 扩展阅读