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

Chrome 扩展开发完全指南 / 第 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 扩展阅读