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);
}
安全与隐私考量
-
权限滥用防护:
- 仅在用户明确操作后请求权限
- 解释通知用途的价值主张
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(); } }); } -
内容安全策略:
- 限制通知中的外部资源
- 验证数据来源
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; } -
频率限制:
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
上一篇:9.3 全屏API
下一篇:9.5 电池状态与网络信息