剪贴板与通知 API(Clipboard and Notification APIs)
一、Clipboard API
1.1 剪贴板 API 概述
Clipboard API 提供了读写系统剪贴板的能力。现代浏览器出于安全考虑,要求剪贴板操作必须在用户交互(如点击事件)的上下文中触发,并且需要用户授权。
// 检测剪贴板 API 支持
if ('clipboard' in navigator) {
console.log('Clipboard API supported');
}
1.2 读取剪贴板
// 读取文本
async function readClipboardText() {
try {
const text = await navigator.clipboard.readText();
console.log('Clipboard text:', text);
return text;
} catch (error) {
console.error('Failed to read clipboard:', error);
}
}
// 读取图片(需要权限)
async function readClipboardImage() {
try {
const items = await navigator.clipboard.read();
for (const item of items) {
for (const type of item.types) {
if (type.startsWith('image/')) {
const blob = await item.getType(type);
return URL.createObjectURL(blob);
}
}
}
} catch (error) {
console.error('Failed to read image:', error);
}
}
// 点击事件中读取
document.getElementById('pasteBtn').addEventListener('click', async () => {
const text = await navigator.clipboard.readText();
console.log('Pasted:', text);
});
1.3 写入剪贴板
// 写入文本
async function copyToClipboard(text) {
try {
await navigator.clipboard.writeText(text);
console.log('Copied to clipboard');
} catch (error) {
console.error('Failed to copy:', error);
}
}
// 写入富文本
async function copyRichText(html, plainText) {
try {
const blob = new Blob([html], { type: 'text/html' });
const textBlob = new Blob([plainText], { type: 'text/plain' });
const item = new ClipboardItem({
'text/html': blob,
'text/plain': textBlob
});
await navigator.clipboard.write([item]);
console.log('Rich text copied');
} catch (error) {
console.error('Failed to copy rich text:', error);
}
}
// 写入图片
async function copyImage(imageUrl) {
try {
const response = await fetch(imageUrl);
const blob = await response.blob();
const item = new ClipboardItem({
[blob.type]: blob
});
await navigator.clipboard.write([item]);
console.log('Image copied');
} catch (error) {
console.error('Failed to copy image:', error);
}
}
二、Clipboard 事件
2.1 传统剪贴板事件
// 复制事件
document.addEventListener('copy', (e) => {
console.log('Copy action detected');
// 修改剪贴板内容
e.preventDefault(); // 阻止默认行为后可以修改
e.clipboardData.setData('text/plain', 'Custom copied text');
e.clipboardData.setData('text/html', '<b>Custom copied text</b>');
});
// 剪切事件
document.addEventListener('cut', (e) => {
console.log('Cut action detected');
e.preventDefault();
e.clipboardData.setData('text/plain', 'Cut text');
});
// 粘贴事件
document.addEventListener('paste', (e) => {
console.log('Paste action detected');
e.preventDefault();
const text = e.clipboardData.getData('text/plain');
console.log('Pasted text:', text);
});
2.2 与新 API 的对比
| 特性 | 传统事件 | Async Clipboard API |
|---|---|---|
| 数据类型 | 字符串 | 字符串、Blob、File |
| 异步操作 | 不支持 | 完全支持 |
| 权限模型 | 无 | 需要明确授权 |
| 可靠性 | 同步但可能有延迟 | Promise-based |
三、Notification API
3.1 发送通知
// 检测支持
if ('Notification' in window) {
console.log('Notifications supported');
}
// 检查权限
console.log('Permission:', Notification.permission);
// 'granted' - 已授权
// 'denied' - 已拒绝
// 'default' - 未决定
// 请求权限
async function requestNotificationPermission() {
const permission = await Notification.requestPermission();
console.log('Permission:', permission);
return permission;
}
// 发送通知
function showNotification(title, options = {}) {
if (Notification.permission !== 'granted') {
console.error('Notification permission not granted');
return;
}
const notification = new Notification(title, {
body: options.body || '',
icon: options.icon || '/icon.png',
tag: options.tag || '', // 用于分组和替换
requireInteraction: options.requireInteraction || false,
data: options.data || {},
...options
});
// 监听点击
notification.addEventListener('click', (e) => {
console.log('Notification clicked');
// 打开窗口或聚焦
window.focus();
notification.close();
});
// 监听关闭
notification.addEventListener('close', (e) => {
console.log('Notification closed');
});
return notification;
}
3.2 通知选项详解
// 完整选项
const options = {
body: '通知正文内容',
icon: '/icon.png',
badge: '/badge.png', // 状态栏小图标
tag: 'notification-id', // 唯一标识,用于去重
renotify: true, // 是否在已有 tag 时重新通知
requireInteraction: true, // 是否保持直到用户交互
silent: false, // 是否静音
vibrate: [200, 100, 200], // 振动模式
timestamp: Date.now(), // 时间戳
lang: 'zh-CN', // 语言
dir: 'ltr', // 文字方向
data: { url: '/some-page' } // 自定义数据
};
四、Service Worker 中的通知
4.1 后台通知
// sw.js
self.addEventListener('push', (event) => {
const data = event.data ? event.data.json() : {};
const options = {
body: data.body || 'New notification',
icon: data.icon || '/icon.png',
badge: data.badge || '/badge.png',
tag: data.tag || 'default',
data: data.data || {},
actions: data.actions || [],
requireInteraction: data.requireInteraction || false
};
event.waitUntil(
self.registration.showNotification(data.title || 'Notification', options)
);
});
// 点击通知
self.addEventListener('notificationclick', (event) => {
event.notification.close();
const url = event.notification.data.url || '/';
event.waitUntil(
clients.matchAll({ type: 'window', includeUncontrolled: true })
.then((clientList) => {
// 如果有窗口则聚焦,否则打开新窗口
for (const client of clientList) {
if (client.url === url && 'focus' in client) {
return client.focus();
}
}
return clients.openWindow(url);
})
);
});
4.2 定时通知
// 安排定时通知
async function scheduleNotification(title, options, delay = 0) {
// 在 Service Worker 中使用
const now = Date.now();
const timestamp = now + delay;
// 使用 Web Push 或后台同步来实现
// 这里只是简单示例
setTimeout(() => {
showNotification(title, options);
}, delay);
}
五、综合示例:剪贴板 + 通知
5.1 复制成功通知
class ClipboardNotifier {
constructor() {
this.init();
}
init() {
document.addEventListener('click', (e) => {
const copyButton = e.target.closest('[data-copy]');
if (copyButton) {
this.handleCopy(copyButton);
}
});
}
async handleCopy(element) {
const text = element.dataset.copy || element.textContent;
try {
await navigator.clipboard.writeText(text);
this.showNotification('已复制', {
body: text.slice(0, 50) + (text.length > 50 ? '...' : ''),
tag: 'copy-success',
silent: true
});
} catch (error) {
console.error('Copy failed:', error);
}
}
showNotification(title, options) {
if (Notification.permission === 'granted') {
new Notification(title, options);
} else if (Notification.permission !== 'denied') {
Notification.requestPermission().then(permission => {
if (permission === 'granted') {
new Notification(title, options);
}
});
}
}
}
// 使用
// <button data-copy="要复制的内容">复制</button>
const notifier = new ClipboardNotifier();
六、安全考虑
6.1 权限请求
// 始终在用户交互后请求权限
async function safeRequestPermission() {
// 不要在页面加载时自动请求
// 等待用户点击或交互
if (Notification.permission === 'default') {
const btn = document.getElementById('enableNotifications');
btn.addEventListener('click', async () => {
const permission = await Notification.requestPermission();
console.log('Permission:', permission);
});
}
}
6.2 恶意使用风险
// 恶意网站可能利用剪贴板读取敏感信息
// 传统 paste 事件可以被滥用
document.addEventListener('paste', (e) => {
// 不要在这里记录或上传剪贴板内容
// 这可能被用于钓鱼攻击
});
// 最佳实践:只读取用户明确触发的剪贴板操作