第 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 扩展阅读