通知API

通知API概述

通知API(Notification API)允许网页向用户显示桌面通知,即使网页不在前台运行。这对于即时通讯、邮件提醒、任务提醒等场景非常有用。

东巴文(db-w.cn) 认为:通知API让Web应用具备了原生应用的推送能力,是提升用户体验的重要工具。

通知API特点

核心特点

特点 说明
桌面通知 在系统通知区域显示通知
后台运行 即使页面不在前台也能显示
自定义样式 可以自定义图标、标题、内容等
交互支持 支持点击、关闭等交互事件
权限管理 需要用户授权才能显示

通知类型

// 通知类型
const NotificationTypes = {
    Basic: '基本通知',
    Image: '图片通知',
    List: '列表通知',
    Progress: '进度通知'
};

// 通知权限
const NotificationPermissions = {
    default: '默认(未询问)',
    granted: '已授权',
    denied: '已拒绝'
};

权限管理

请求权限

<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>通知权限管理</title>
    <style>
        body {
            font-family: Arial, sans-serif;
            max-width: 800px;
            margin: 50px auto;
            padding: 20px;
        }
        
        .permission-panel {
            padding: 30px;
            background: #f9f9f9;
            border-radius: 10px;
            text-align: center;
            margin: 20px 0;
        }
        
        .permission-status {
            font-size: 48px;
            margin-bottom: 20px;
        }
        
        .permission-status.granted {
            color: #28a745;
        }
        
        .permission-status.denied {
            color: #dc3545;
        }
        
        .permission-status.default {
            color: #ffc107;
        }
        
        .permission-text {
            font-size: 18px;
            color: #666;
            margin-bottom: 20px;
        }
        
        button {
            padding: 15px 30px;
            font-size: 16px;
            background: #007bff;
            color: white;
            border: none;
            border-radius: 5px;
            cursor: pointer;
            margin: 5px;
        }
        
        button:hover {
            background: #0056b3;
        }
        
        button:disabled {
            background: #ccc;
            cursor: not-allowed;
        }
        
        .info-box {
            background: #e7f3ff;
            border-left: 4px solid #007bff;
            padding: 15px;
            margin: 20px 0;
            border-radius: 5px;
        }
    </style>
</head>
<body>
    <h1>通知权限管理</h1>
    
    <div class="permission-panel">
        <div class="permission-status" id="permissionIcon"></div>
        <div class="permission-text" id="permissionText">检查权限状态...</div>
        <button id="requestBtn" onclick="requestPermission()">请求通知权限</button>
        <button onclick="checkPermission()">检查权限</button>
    </div>
    
    <div class="info-box">
        <strong>说明:</strong>
        <ul>
            <li>浏览器需要用户明确授权才能显示通知</li>
            <li>用户可以随时在浏览器设置中更改权限</li>
            <li>如果用户拒绝,需要引导用户手动开启</li>
        </ul>
    </div>
    
    <script>
        // 检查浏览器支持
        if (!('Notification' in window)) {
            document.getElementById('permissionText').textContent = 
                '您的浏览器不支持通知功能';
            document.getElementById('requestBtn').disabled = true;
        }
        
        // 检查权限状态
        function checkPermission() {
            const permission = Notification.permission;
            const icon = document.getElementById('permissionIcon');
            const text = document.getElementById('permissionText');
            const requestBtn = document.getElementById('requestBtn');
            
            switch (permission) {
                case 'granted':
                    icon.textContent = '✅';
                    icon.className = 'permission-status granted';
                    text.textContent = '通知权限已授权';
                    requestBtn.textContent = '发送测试通知';
                    requestBtn.onclick = sendTestNotification;
                    break;
                    
                case 'denied':
                    icon.textContent = '❌';
                    icon.className = 'permission-status denied';
                    text.textContent = '通知权限被拒绝。请在浏览器设置中手动开启。';
                    requestBtn.disabled = true;
                    break;
                    
                default:
                    icon.textContent = '❓';
                    icon.className = 'permission-status default';
                    text.textContent = '通知权限未设置';
                    requestBtn.textContent = '请求通知权限';
                    requestBtn.onclick = requestPermission;
                    requestBtn.disabled = false;
            }
        }
        
        // 请求权限
        function requestPermission() {
            Notification.requestPermission()
                .then(permission => {
                    checkPermission();
                    
                    if (permission === 'granted') {
                        sendTestNotification();
                    }
                });
        }
        
        // 发送测试通知
        function sendTestNotification() {
            const notification = new Notification('测试通知', {
                body: '通知权限已成功授权!',
                icon: 'https://db-w.cn/logo.png'
            });
            
            notification.onclick = function() {
                window.focus();
                notification.close();
            };
        }
        
        // 初始检查
        checkPermission();
    </script>
</body>
</html>

创建通知

基本通知

// 基本通知
const notification = new Notification('通知标题', {
    body: '通知内容',
    icon: 'https://example.com/icon.png'
});

// 通知选项
const options = {
    // 基本选项
    body: '通知正文内容',
    icon: '图标URL',
    image: '大图片URL',
    badge: '徽章URL',
    
    // 方向和语言
    dir: 'auto', // auto, ltr, rtl
    lang: 'zh-CN',
    
    // 标签和重新通知
    tag: 'unique-id', // 相同tag的通知会替换
    renotify: true, // 替换时是否重新通知
    
    // 交互
    requireInteraction: true, // 是否需要用户交互才能关闭
    silent: false, // 是否静音
    
    // 数据
    data: {
        url: 'https://example.com',
        id: 123
    },
    
    // 操作按钮(Service Worker中)
    actions: [
        {
            action: 'open',
            title: '打开'
        },
        {
            action: 'close',
            title: '关闭'
        }
    ],
    
    // 振动(移动设备)
    vibrate: [200, 100, 200]
};

完整示例

<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>创建通知示例</title>
    <style>
        body {
            font-family: Arial, sans-serif;
            max-width: 800px;
            margin: 50px auto;
            padding: 20px;
        }
        
        .form-group {
            margin: 15px 0;
        }
        
        label {
            display: block;
            margin-bottom: 5px;
            font-weight: bold;
        }
        
        input[type="text"],
        textarea,
        select {
            width: 100%;
            padding: 10px;
            border: 1px solid #ddd;
            border-radius: 5px;
            font-size: 14px;
        }
        
        textarea {
            height: 80px;
            resize: vertical;
        }
        
        .checkbox-group {
            display: flex;
            gap: 20px;
            flex-wrap: wrap;
        }
        
        .checkbox-group label {
            display: flex;
            align-items: center;
            font-weight: normal;
        }
        
        .checkbox-group input {
            margin-right: 5px;
        }
        
        button {
            padding: 12px 30px;
            background: #007bff;
            color: white;
            border: none;
            border-radius: 5px;
            cursor: pointer;
            font-size: 16px;
            margin-top: 10px;
        }
        
        button:hover {
            background: #0056b3;
        }
        
        .notification-log {
            margin-top: 30px;
            padding: 20px;
            background: #f9f9f9;
            border-radius: 5px;
        }
        
        .log-entry {
            padding: 10px;
            margin: 5px 0;
            background: white;
            border-radius: 5px;
            border-left: 4px solid #007bff;
        }
        
        .log-entry.click {
            border-left-color: #28a745;
        }
        
        .log-entry.close {
            border-left-color: #dc3545;
        }
        
        .log-entry.error {
            border-left-color: #ffc107;
        }
    </style>
</head>
<body>
    <h1>创建通知示例</h1>
    
    <div class="form-group">
        <label for="title">通知标题</label>
        <input type="text" id="title" value="新消息通知" placeholder="输入通知标题">
    </div>
    
    <div class="form-group">
        <label for="body">通知内容</label>
        <textarea id="body" placeholder="输入通知内容">您有一条新消息,点击查看详情。</textarea>
    </div>
    
    <div class="form-group">
        <label for="icon">图标URL(可选)</label>
        <input type="text" id="icon" placeholder="https://example.com/icon.png">
    </div>
    
    <div class="form-group">
        <label for="tag">标签(可选)</label>
        <input type="text" id="tag" placeholder="相同标签的通知会替换">
    </div>
    
    <div class="form-group">
        <label>通知选项</label>
        <div class="checkbox-group">
            <label>
                <input type="checkbox" id="requireInteraction">
                需要用户交互
            </label>
            <label>
                <input type="checkbox" id="silent">
                静音
            </label>
            <label>
                <input type="checkbox" id="renotify">
                替换时重新通知
            </label>
        </div>
    </div>
    
    <button onclick="createNotification()">创建通知</button>
    <button onclick="createMultipleNotifications()">创建多个通知</button>
    
    <div class="notification-log">
        <h3>通知日志</h3>
        <div id="log"></div>
    </div>
    
    <script>
        // 检查权限
        function checkPermission() {
            if (!('Notification' in window)) {
                addLog('错误', '浏览器不支持通知功能', 'error');
                return false;
            }
            
            if (Notification.permission === 'denied') {
                addLog('错误', '通知权限被拒绝', 'error');
                return false;
            }
            
            if (Notification.permission === 'default') {
                Notification.requestPermission().then(permission => {
                    if (permission === 'granted') {
                        addLog('系统', '通知权限已授权');
                    }
                });
                return false;
            }
            
            return true;
        }
        
        // 创建通知
        function createNotification() {
            if (!checkPermission()) return;
            
            const title = document.getElementById('title').value || '通知';
            const options = {
                body: document.getElementById('body').value,
                icon: document.getElementById('icon').value || undefined,
                tag: document.getElementById('tag').value || undefined,
                requireInteraction: document.getElementById('requireInteraction').checked,
                silent: document.getElementById('silent').checked,
                renotify: document.getElementById('renotify').checked,
                data: {
                    timestamp: Date.now()
                }
            };
            
            try {
                const notification = new Notification(title, options);
                
                // 点击事件
                notification.onclick = function(event) {
                    addLog('点击', `通知被点击: ${title}`, 'click');
                    window.focus();
                    notification.close();
                };
                
                // 关闭事件
                notification.onclose = function() {
                    addLog('关闭', `通知已关闭: ${title}`, 'close');
                };
                
                // 错误事件
                notification.onerror = function(error) {
                    addLog('错误', `通知错误: ${error}`, 'error');
                };
                
                addLog('创建', `通知已创建: ${title}`);
                
            } catch (error) {
                addLog('错误', `创建失败: ${error.message}`, 'error');
            }
        }
        
        // 创建多个通知
        function createMultipleNotifications() {
            if (!checkPermission()) return;
            
            const notifications = [
                { title: '消息通知', body: '您有3条新消息' },
                { title: '邮件通知', body: '您收到一封新邮件' },
                { title: '提醒通知', body: '会议将在10分钟后开始' }
            ];
            
            notifications.forEach((n, i) => {
                setTimeout(() => {
                    new Notification(n.title, {
                        body: n.body,
                        tag: `notification-${i}`
                    });
                    addLog('创建', `批量通知: ${n.title}`);
                }, i * 500);
            });
        }
        
        // 添加日志
        function addLog(type, message, className = '') {
            const log = document.getElementById('log');
            const entry = document.createElement('div');
            entry.className = 'log-entry ' + className;
            entry.innerHTML = `
                <strong>[${new Date().toLocaleTimeString()}]</strong>
                <strong>${type}:</strong> ${message}
            `;
            log.insertBefore(entry, log.firstChild);
        }
        
        // 初始检查
        if (!('Notification' in window)) {
            addLog('错误', '浏览器不支持通知功能', 'error');
        } else {
            addLog('系统', `当前权限状态: ${Notification.permission}`);
        }
    </script>
</body>
</html>

通知事件

事件处理

// 创建通知并处理事件
const notification = new Notification('事件示例', {
    body: '点击或关闭此通知',
    requireInteraction: true
});

// 显示事件
notification.onshow = function(event) {
    console.log('通知已显示');
};

// 点击事件
notification.onclick = function(event) {
    console.log('通知被点击');
    console.log('通知数据:', event.target.data);
    
    // 打开窗口或切换焦点
    window.focus();
    
    // 关闭通知
    notification.close();
    
    // 执行其他操作
    handleNotificationClick(event.target.data);
};

// 关闭事件
notification.onclose = function(event) {
    console.log('通知已关闭');
};

// 错误事件
notification.onerror = function(event) {
    console.error('通知错误:', event);
};

// 处理点击
function handleNotificationClick(data) {
    if (data && data.url) {
        window.open(data.url, '_blank');
    }
}

Service Worker通知

在Service Worker中显示通知

// 注册Service Worker
if ('serviceWorker' in navigator) {
    navigator.serviceWorker.register('/sw.js')
        .then(registration => {
            console.log('Service Worker注册成功');
        });
}

// sw.js - Service Worker文件
self.addEventListener('push', function(event) {
    const options = {
        body: event.data ? event.data.text() : '新消息',
        icon: '/icon.png',
        badge: '/badge.png',
        vibrate: [100, 50, 100],
        data: {
            dateOfArrival: Date.now(),
            primaryKey: 1
        },
        actions: [
            {
                action: 'explore',
                title: '查看详情',
                icon: '/check.png'
            },
            {
                action: 'close',
                title: '关闭',
                icon: '/close.png'
            }
        ]
    };
    
    event.waitUntil(
        self.registration.showNotification('推送通知', options)
    );
});

// 处理通知点击
self.addEventListener('notificationclick', function(event) {
    event.notification.close();
    
    if (event.action === 'explore') {
        // 打开特定页面
        event.waitUntil(
            clients.openWindow('/details')
        );
    } else if (event.action === 'close') {
        // 关闭通知
        console.log('用户关闭了通知');
    } else {
        // 点击通知主体
        event.waitUntil(
            clients.openWindow('/')
        );
    }
});

综合示例

<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>通知中心 - 东巴文</title>
    <style>
        * {
            margin: 0;
            padding: 0;
            box-sizing: border-box;
        }
        
        body {
            font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
            background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
            min-height: 100vh;
            padding: 20px;
        }
        
        .container {
            max-width: 1000px;
            margin: 0 auto;
        }
        
        .header {
            text-align: center;
            color: white;
            padding: 30px 0;
        }
        
        .header h1 {
            font-size: 36px;
            margin-bottom: 10px;
        }
        
        .header p {
            font-size: 18px;
            opacity: 0.9;
        }
        
        .card {
            background: white;
            border-radius: 15px;
            padding: 30px;
            margin: 20px 0;
            box-shadow: 0 10px 30px rgba(0, 0, 0, 0.2);
        }
        
        .card h2 {
            margin-bottom: 20px;
            color: #333;
        }
        
        .status-panel {
            display: grid;
            grid-template-columns: repeat(3, 1fr);
            gap: 20px;
            margin-bottom: 20px;
        }
        
        .status-item {
            text-align: center;
            padding: 20px;
            background: #f9f9f9;
            border-radius: 10px;
        }
        
        .status-icon {
            font-size: 48px;
            margin-bottom: 10px;
        }
        
        .status-label {
            font-size: 14px;
            color: #666;
        }
        
        .status-value {
            font-size: 18px;
            font-weight: bold;
            color: #333;
        }
        
        .notification-types {
            display: grid;
            grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
            gap: 15px;
            margin: 20px 0;
        }
        
        .notification-btn {
            padding: 20px;
            background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
            color: white;
            border: none;
            border-radius: 10px;
            cursor: pointer;
            transition: transform 0.3s;
            font-size: 16px;
        }
        
        .notification-btn:hover {
            transform: translateY(-5px);
        }
        
        .notification-btn:active {
            transform: translateY(0);
        }
        
        .notification-btn .icon {
            font-size: 24px;
            display: block;
            margin-bottom: 10px;
        }
        
        .history {
            max-height: 400px;
            overflow-y: auto;
        }
        
        .history-item {
            display: flex;
            align-items: center;
            padding: 15px;
            border-bottom: 1px solid #eee;
            transition: background 0.3s;
        }
        
        .history-item:hover {
            background: #f9f9f9;
        }
        
        .history-icon {
            font-size: 24px;
            margin-right: 15px;
        }
        
        .history-content {
            flex: 1;
        }
        
        .history-title {
            font-weight: bold;
            margin-bottom: 5px;
        }
        
        .history-body {
            font-size: 14px;
            color: #666;
        }
        
        .history-time {
            font-size: 12px;
            color: #999;
        }
        
        .empty-state {
            text-align: center;
            padding: 40px;
            color: #999;
        }
        
        .permission-request {
            text-align: center;
            padding: 40px;
        }
        
        .permission-request button {
            padding: 15px 40px;
            font-size: 18px;
            background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
            color: white;
            border: none;
            border-radius: 10px;
            cursor: pointer;
            margin-top: 20px;
        }
        
        .permission-request button:hover {
            opacity: 0.9;
        }
    </style>
</head>
<body>
    <div class="container">
        <div class="header">
            <h1>🔔 通知中心</h1>
            <p>管理和发送桌面通知</p>
        </div>
        
        <div class="card">
            <h2>权限状态</h2>
            <div class="status-panel">
                <div class="status-item">
                    <div class="status-icon" id="permissionIcon"></div>
                    <div class="status-label">权限状态</div>
                    <div class="status-value" id="permissionText">检查中...</div>
                </div>
                <div class="status-item">
                    <div class="status-icon">📊</div>
                    <div class="status-label">已发送通知</div>
                    <div class="status-value" id="sentCount">0</div>
                </div>
                <div class="status-item">
                    <div class="status-icon">👆</div>
                    <div class="status-label">已点击通知</div>
                    <div class="status-value" id="clickCount">0</div>
                </div>
            </div>
        </div>
        
        <div class="card" id="permissionCard" style="display: none;">
            <div class="permission-request">
                <h2>需要通知权限</h2>
                <p>点击下方按钮授权通知权限</p>
                <button onclick="requestPermission()">授权通知权限</button>
            </div>
        </div>
        
        <div class="card" id="notificationPanel" style="display: none;">
            <h2>发送通知</h2>
            <div class="notification-types">
                <button class="notification-btn" onclick="sendNotification('message')">
                    <span class="icon">💬</span>
                    消息通知
                </button>
                <button class="notification-btn" onclick="sendNotification('email')">
                    <span class="icon">📧</span>
                    邮件通知
                </button>
                <button class="notification-btn" onclick="sendNotification('reminder')">
                    <span class="icon"></span>
                    提醒通知
                </button>
                <button class="notification-btn" onclick="sendNotification('alert')">
                    <span class="icon">⚠️</span>
                    警告通知
                </button>
                <button class="notification-btn" onclick="sendNotification('success')">
                    <span class="icon"></span>
                    成功通知
                </button>
                <button class="notification-btn" onclick="sendNotification('promotion')">
                    <span class="icon">🎉</span>
                    活动通知
                </button>
            </div>
        </div>
        
        <div class="card">
            <h2>通知历史</h2>
            <div class="history" id="history">
                <div class="empty-state">暂无通知记录</div>
            </div>
        </div>
    </div>
    
    <script>
        // 通知中心
        class NotificationCenter {
            constructor() {
                this.sentCount = 0;
                this.clickCount = 0;
                this.history = [];
                
                this.init();
            }
            
            init() {
                this.checkPermission();
            }
            
            checkPermission() {
                if (!('Notification' in window)) {
                    this.updatePermissionUI('unsupported');
                    return;
                }
                
                const permission = Notification.permission;
                this.updatePermissionUI(permission);
                
                if (permission === 'granted') {
                    document.getElementById('notificationPanel').style.display = 'block';
                    document.getElementById('permissionCard').style.display = 'none';
                } else if (permission === 'default') {
                    document.getElementById('notificationPanel').style.display = 'none';
                    document.getElementById('permissionCard').style.display = 'block';
                } else {
                    document.getElementById('notificationPanel').style.display = 'none';
                    document.getElementById('permissionCard').style.display = 'block';
                }
            }
            
            requestPermission() {
                Notification.requestPermission()
                    .then(permission => {
                        this.checkPermission();
                        
                        if (permission === 'granted') {
                            this.sendWelcomeNotification();
                        }
                    });
            }
            
            updatePermissionUI(permission) {
                const icon = document.getElementById('permissionIcon');
                const text = document.getElementById('permissionText');
                
                const permissionMap = {
                    granted: { icon: '✅', text: '已授权' },
                    denied: { icon: '❌', text: '已拒绝' },
                    default: { icon: '❓', text: '未设置' },
                    unsupported: { icon: '🚫', text: '不支持' }
                };
                
                const { icon: iconText, text: textValue } = permissionMap[permission];
                icon.textContent = iconText;
                text.textContent = textValue;
            }
            
            sendWelcomeNotification() {
                const notification = new Notification('欢迎!', {
                    body: '通知功能已成功启用',
                    icon: 'https://db-w.cn/logo.png'
                });
                
                this.addToHistory('欢迎', '通知功能已成功启用', '✅');
            }
            
            sendNotification(type) {
                if (Notification.permission !== 'granted') {
                    alert('请先授权通知权限');
                    return;
                }
                
                const notifications = {
                    message: {
                        title: '新消息',
                        body: '您有一条新消息,点击查看详情',
                        icon: '💬'
                    },
                    email: {
                        title: '新邮件',
                        body: '您收到一封来自张三的邮件',
                        icon: '📧'
                    },
                    reminder: {
                        title: '日程提醒',
                        body: '会议将在10分钟后开始',
                        icon: '⏰'
                    },
                    alert: {
                        title: '系统警告',
                        body: '您的存储空间即将用尽',
                        icon: '⚠️'
                    },
                    success: {
                        title: '操作成功',
                        body: '文件已成功上传到云端',
                        icon: '✅'
                    },
                    promotion: {
                        title: '限时活动',
                        body: '双十一大促,全场5折起!',
                        icon: '🎉'
                    }
                };
                
                const config = notifications[type];
                const notification = new Notification(config.title, {
                    body: config.body,
                    tag: type,
                    requireInteraction: true,
                    data: {
                        type: type,
                        timestamp: Date.now()
                    }
                });
                
                // 更新计数
                this.sentCount++;
                document.getElementById('sentCount').textContent = this.sentCount;
                
                // 添加历史
                this.addToHistory(config.title, config.body, config.icon);
                
                // 点击事件
                notification.onclick = (event) => {
                    this.clickCount++;
                    document.getElementById('clickCount').textContent = this.clickCount;
                    window.focus();
                    notification.close();
                };
            }
            
            addToHistory(title, body, icon) {
                const history = document.getElementById('history');
                
                // 移除空状态
                const emptyState = history.querySelector('.empty-state');
                if (emptyState) {
                    emptyState.remove();
                }
                
                const item = document.createElement('div');
                item.className = 'history-item';
                item.innerHTML = `
                    <div class="history-icon">${icon}</div>
                    <div class="history-content">
                        <div class="history-title">${title}</div>
                        <div class="history-body">${body}</div>
                    </div>
                    <div class="history-time">${new Date().toLocaleTimeString()}</div>
                `;
                
                history.insertBefore(item, history.firstChild);
                
                // 保存到历史记录
                this.history.unshift({
                    title,
                    body,
                    icon,
                    time: new Date()
                });
                
                // 限制历史记录数量
                if (this.history.length > 50) {
                    this.history.pop();
                }
            }
        }
        
        // 初始化通知中心
        const notificationCenter = new NotificationCenter();
    </script>
</body>
</html>

最佳实践

1. 权限请求时机

// 推荐:在用户操作后请求权限
document.getElementById('enableNotifications').addEventListener('click', function() {
    Notification.requestPermission().then(permission => {
        if (permission === 'granted') {
            // 显示欢迎通知
            new Notification('感谢授权!', {
                body: '您将收到重要通知'
            });
        }
    });
});

// 不推荐:页面加载时立即请求
window.addEventListener('load', function() {
    Notification.requestPermission(); // 用户可能不理解为什么要授权
});

2. 通知内容设计

// 推荐:简洁明了的通知
const goodNotification = new Notification('新消息', {
    body: '张三: 你好,明天的会议改到下午3点',
    icon: '/icon.png',
    tag: 'message-123'
});

// 不推荐:过长或模糊的通知
const badNotification = new Notification('系统通知', {
    body: '这是一条非常长的通知内容,包含了很多不必要的细节,用户可能没有耐心读完...', // 太长
    // 没有图标,没有标签
});

3. 通知频率控制

// 推荐:控制通知频率
class NotificationManager {
    constructor() {
        this.lastNotificationTime = 0;
        this.minInterval = 5000; // 最小间隔5秒
    }
    
    send(title, options) {
        const now = Date.now();
        if (now - this.lastNotificationTime < this.minInterval) {
            console.log('通知发送过于频繁,已忽略');
            return false;
        }
        
        this.lastNotificationTime = now;
        return new Notification(title, options);
    }
}

const manager = new NotificationManager();
manager.send('通知1', { body: '内容1' });
manager.send('通知2', { body: '内容2' }); // 可能被忽略

东巴文点评:通知API使用要遵循"少而精"原则,避免打扰用户。

学习检验

知识点测试

问题1:以下哪个方法用于请求通知权限?

A. Notification.askPermission() B. Notification.requestPermission() C. Notification.getPermission() D. Notification.enable()

<details> <summary>点击查看答案</summary>

答案:B

东巴文解释:使用Notification.requestPermission()方法请求用户授权。该方法返回一个Promise,resolve值为'granted'、'denied'或'default'。

</details>

问题2:Notification.permission的值为'denied'表示什么?

A. 用户尚未决定 B. 用户已授权 C. 用户已拒绝 D. 浏览器不支持

<details> <summary>点击查看答案</summary>

答案:C

东巴文解释:Notification.permission有三个可能的值:'default'(用户尚未决定)、'granted'(用户已授权)、'denied'(用户已拒绝)。'denied'表示用户明确拒绝了通知权限。

</details>

实践任务

任务:创建一个番茄钟应用,使用通知API在番茄钟结束时提醒用户。

<details> <summary>点击查看参考答案</summary>
<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>番茄钟 - 东巴文</title>
    <style>
        * {
            margin: 0;
            padding: 0;
            box-sizing: border-box;
        }
        
        body {
            font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
            background: linear-gradient(135deg, #ff6b6b 0%, #ee5a6f 100%);
            min-height: 100vh;
            display: flex;
            align-items: center;
            justify-content: center;
            padding: 20px;
        }
        
        .pomodoro-container {
            background: white;
            border-radius: 20px;
            padding: 40px;
            box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
            text-align: center;
            max-width: 400px;
            width: 100%;
        }
        
        h1 {
            color: #ee5a6f;
            margin-bottom: 10px;
        }
        
        .subtitle {
            color: #666;
            margin-bottom: 30px;
        }
        
        .timer {
            position: relative;
            width: 250px;
            height: 250px;
            margin: 0 auto 30px;
        }
        
        .timer-circle {
            width: 100%;
            height: 100%;
            border-radius: 50%;
            background: #f9f9f9;
            display: flex;
            align-items: center;
            justify-content: center;
            position: relative;
        }
        
        .timer-progress {
            position: absolute;
            top: 0;
            left: 0;
            width: 100%;
            height: 100%;
        }
        
        .timer-text {
            font-size: 48px;
            font-weight: bold;
            color: #333;
            z-index: 1;
        }
        
        .timer-label {
            font-size: 14px;
            color: #999;
            margin-top: 5px;
        }
        
        .controls {
            display: flex;
            gap: 10px;
            justify-content: center;
            margin-bottom: 20px;
        }
        
        button {
            padding: 15px 30px;
            font-size: 16px;
            border: none;
            border-radius: 10px;
            cursor: pointer;
            transition: all 0.3s;
        }
        
        .btn-primary {
            background: linear-gradient(135deg, #ff6b6b 0%, #ee5a6f 100%);
            color: white;
        }
        
        .btn-primary:hover {
            transform: translateY(-2px);
            box-shadow: 0 5px 20px rgba(238, 90, 111, 0.4);
        }
        
        .btn-secondary {
            background: #f9f9f9;
            color: #666;
        }
        
        .btn-secondary:hover {
            background: #eee;
        }
        
        .stats {
            display: grid;
            grid-template-columns: repeat(3, 1fr);
            gap: 15px;
            margin-top: 20px;
            padding-top: 20px;
            border-top: 1px solid #eee;
        }
        
        .stat-item {
            text-align: center;
        }
        
        .stat-value {
            font-size: 24px;
            font-weight: bold;
            color: #ee5a6f;
        }
        
        .stat-label {
            font-size: 12px;
            color: #999;
        }
        
        .notification-toggle {
            margin-top: 20px;
            padding: 15px;
            background: #f9f9f9;
            border-radius: 10px;
        }
        
        .notification-toggle label {
            display: flex;
            align-items: center;
            justify-content: center;
            gap: 10px;
            cursor: pointer;
        }
        
        .notification-toggle input {
            width: 20px;
            height: 20px;
        }
    </style>
</head>
<body>
    <div class="pomodoro-container">
        <h1>🍅 番茄钟</h1>
        <p class="subtitle">专注工作,高效休息</p>
        
        <div class="timer">
            <div class="timer-circle">
                <svg class="timer-progress" viewBox="0 0 250 250">
                    <circle cx="125" cy="125" r="120" fill="none" stroke="#eee" stroke-width="8"/>
                    <circle id="progressCircle" cx="125" cy="125" r="120" fill="none" 
                            stroke="#ee5a6f" stroke-width="8" 
                            stroke-linecap="round"
                            stroke-dasharray="753.98"
                            stroke-dashoffset="0"
                            transform="rotate(-90 125 125)"/>
                </svg>
                <div>
                    <div class="timer-text" id="timerDisplay">25:00</div>
                    <div class="timer-label" id="timerLabel">工作时间</div>
                </div>
            </div>
        </div>
        
        <div class="controls">
            <button class="btn-primary" id="startBtn" onclick="startTimer()">开始</button>
            <button class="btn-secondary" id="resetBtn" onclick="resetTimer()">重置</button>
        </div>
        
        <div class="stats">
            <div class="stat-item">
                <div class="stat-value" id="completedCount">0</div>
                <div class="stat-label">已完成</div>
            </div>
            <div class="stat-item">
                <div class="stat-value" id="totalTime">0</div>
                <div class="stat-label">总时长(分钟)</div>
            </div>
            <div class="stat-item">
                <div class="stat-value" id="currentStreak">0</div>
                <div class="stat-label">连续完成</div>
            </div>
        </div>
        
        <div class="notification-toggle">
            <label>
                <input type="checkbox" id="notificationToggle" checked>
                <span>启用桌面通知</span>
            </label>
        </div>
    </div>
    
    <script>
        // 番茄钟类
        class PomodoroTimer {
            constructor() {
                this.workDuration = 25 * 60; // 25分钟
                this.breakDuration = 5 * 60; // 5分钟
                this.timeLeft = this.workDuration;
                this.isRunning = false;
                this.isWorkTime = true;
                this.timer = null;
                
                this.completedCount = 0;
                this.totalMinutes = 0;
                this.currentStreak = 0;
                
                this.init();
            }
            
            init() {
                this.checkNotificationPermission();
                this.updateDisplay();
            }
            
            checkNotificationPermission() {
                if (!('Notification' in window)) {
                    document.getElementById('notificationToggle').disabled = true;
                    return;
                }
                
                if (Notification.permission === 'default') {
                    Notification.requestPermission();
                }
            }
            
            start() {
                if (this.isRunning) {
                    this.pause();
                    return;
                }
                
                this.isRunning = true;
                document.getElementById('startBtn').textContent = '暂停';
                
                this.timer = setInterval(() => {
                    this.timeLeft--;
                    this.updateDisplay();
                    
                    if (this.timeLeft <= 0) {
                        this.complete();
                    }
                }, 1000);
            }
            
            pause() {
                this.isRunning = false;
                clearInterval(this.timer);
                document.getElementById('startBtn').textContent = '继续';
            }
            
            reset() {
                this.isRunning = false;
                clearInterval(this.timer);
                this.timeLeft = this.isWorkTime ? this.workDuration : this.breakDuration;
                document.getElementById('startBtn').textContent = '开始';
                this.updateDisplay();
            }
            
            complete() {
                this.isRunning = false;
                clearInterval(this.timer);
                
                if (this.isWorkTime) {
                    // 工作时间结束
                    this.completedCount++;
                    this.totalMinutes += 25;
                    this.currentStreak++;
                    
                    this.updateStats();
                    this.sendNotification('工作完成!', '休息一下吧 🎉');
                    
                    // 切换到休息时间
                    this.isWorkTime = false;
                    this.timeLeft = this.breakDuration;
                    document.getElementById('timerLabel').textContent = '休息时间';
                } else {
                    // 休息时间结束
                    this.sendNotification('休息结束!', '开始新的番茄钟 🍅');
                    
                    // 切换到工作时间
                    this.isWorkTime = true;
                    this.timeLeft = this.workDuration;
                    document.getElementById('timerLabel').textContent = '工作时间';
                }
                
                document.getElementById('startBtn').textContent = '开始';
                this.updateDisplay();
            }
            
            updateDisplay() {
                const minutes = Math.floor(this.timeLeft / 60);
                const seconds = this.timeLeft % 60;
                
                document.getElementById('timerDisplay').textContent = 
                    `${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}`;
                
                // 更新进度圆环
                const totalTime = this.isWorkTime ? this.workDuration : this.breakDuration;
                const progress = (totalTime - this.timeLeft) / totalTime;
                const circumference = 2 * Math.PI * 120;
                const offset = circumference * (1 - progress);
                
                document.getElementById('progressCircle').style.strokeDashoffset = offset;
            }
            
            updateStats() {
                document.getElementById('completedCount').textContent = this.completedCount;
                document.getElementById('totalTime').textContent = this.totalMinutes;
                document.getElementById('currentStreak').textContent = this.currentStreak;
            }
            
            sendNotification(title, body) {
                if (!document.getElementById('notificationToggle').checked) {
                    return;
                }
                
                if (Notification.permission === 'granted') {
                    const notification = new Notification(title, {
                        body: body,
                        icon: '🍅',
                        tag: 'pomodoro',
                        requireInteraction: true
                    });
                    
                    notification.onclick = () => {
                        window.focus();
                        notification.close();
                    };
                }
            }
        }
        
        // 初始化番茄钟
        const pomodoro = new PomodoroTimer();
        
        function startTimer() {
            pomodoro.start();
        }
        
        function resetTimer() {
            pomodoro.reset();
        }
    </script>
</body>
</html>
</details>

东巴文(db-w.cn) - 让编程学习更有趣、更高效!