Clipboard API 实战:浏览器剪贴板读写与安全策略
技术架构(更新于 2026年6月3日)
Clipboard API:现代剪贴板交互标准
浏览器剪贴板操作经历了从 document.execCommand 到 Async Clipboard API 的演进:
| 对比项 | execCommand('copy') | Async Clipboard API |
|---|---|---|
| 标准 | 已废弃 | W3C 推荐 |
| 异步 | 否(同步阻塞) | 是(Promise) |
| 权限 | 隐式(需用户操作) | 显式 Permissions API |
| 富文本 | 不支持 | 支持(ClipboardItem) |
| 自定义格式 | 不支持 | 支持(web custom formats) |
| 图片读写 | 不支持 | 支持 |
| 可靠性 | 依赖选区 | 直接操作剪贴板 |
核心 API 详解
Clipboard API 接口
// Navigator.clipboard 主接口
interface Clipboard extends EventTarget {
read(): Promise<ClipboardItem[]>;
readText(): Promise<string>;
write(data: ClipboardItem[]): Promise<void>;
writeText(text: string): Promise<void>;
}
// ClipboardItem 表示一条剪贴板数据
interface ClipboardItem {
readonly types: string[];
getType(type: string): Promise<Blob>;
}
文本读写
// 写入文本
async function copyText(text: string) {
try {
await navigator.clipboard.writeText(text);
showToast('已复制到剪贴板');
} catch (err) {
console.error('复制失败:', err);
fallbackCopyText(text);
}
}
// 读取文本
async function pasteText(): Promise<string> {
try {
const text = await navigator.clipboard.readText();
return text;
} catch (err) {
console.error('读取失败:', err);
return '';
}
}
富文本与多格式写入
ClipboardItem 多格式
async function copyRichText(html: string, plainText: string) {
const clipboardItem = new ClipboardItem({
'text/html': new Blob([html], { type: 'text/html' }),
'text/plain': new Blob([plainText], { type: 'text/plain' }),
});
await navigator.clipboard.write([clipboardItem]);
}
// 使用示例
copyRichText(
'<strong>重要通知</strong>:请于 <em>周五</em> 前提交',
'重要通知:请于周五前提交'
);
图片复制
async function copyImage(pngBlob: Blob) {
await navigator.clipboard.write([
new ClipboardItem({ 'image/png': pngBlob })
]);
}
// 从 Canvas 复制
async function copyCanvas(canvas: HTMLCanvasElement) {
const blob = await new Promise<Blob>((resolve) => {
canvas.toBlob((b) => resolve(b!), 'image/png');
});
await navigator.clipboard.write([
new ClipboardItem({ 'image/png': blob })
]);
}
图片粘贴
document.addEventListener('paste', async (e) => {
const items = e.clipboardData?.items;
if (!items) return;
for (const item of items) {
if (item.type.startsWith('image/')) {
const blob = item.getAsFile()!;
const url = URL.createObjectURL(blob);
const img = document.createElement('img');
img.src = url;
document.body.appendChild(img);
}
}
});
自定义 MIME 类型(Web Custom Formats)
async function copyCustomData(data: object) {
const json = JSON.stringify(data);
await navigator.clipboard.write([
new ClipboardItem({
'web application/json': new Blob([json], { type: 'application/json' }),
'text/plain': new Blob([json], { type: 'text/plain' }),
})
]);
}
async function pasteCustomData(): Promise<object | null> {
const items = await navigator.clipboard.read();
for (const item of items) {
if (item.types.includes('web application/json')) {
const blob = await item.getType('web application/json');
const text = await blob.text();
return JSON.parse(text);
}
}
return null;
}
web前缀是自定义格式的安全要求,防止网页读取原生剪贴板格式。
权限与安全策略
Permissions API
async function checkClipboardPermission(): Promise<boolean> {
try {
const result = await navigator.permissions.query({
name: 'clipboard-read' as PermissionName
});
return result.state === 'granted';
} catch {
// 部分浏览器不支持 clipboard-read 权限查询
return false;
}
}
安全限制一览
| 操作 | 要求 | 说明 |
|---|---|---|
writeText |
用户激活 | 需 click/keydown 等用户手势 |
write |
用户激活 | 同上 |
readText |
用户激活 + 权限 | 需用户授权弹窗 |
read |
用户激活 + 权限 | 需用户授权弹窗 |
| 焦点窗口 | 文档需聚焦 | 失焦窗口调用会拒绝 |
用户激活检查
// 确保在用户交互中调用
button.addEventListener('click', async () => {
// ✅ 在 click 事件中,有用户激活
await navigator.clipboard.writeText('copied!');
});
// ❌ 定时器中没有用户激活
setTimeout(async () => {
await navigator.clipboard.writeText('fails'); // DOMException
}, 1000);
降级方案:execCommand
function fallbackCopyText(text: string): boolean {
const textarea = document.createElement('textarea');
textarea.value = text;
textarea.style.position = 'fixed';
textarea.style.left = '-9999px';
document.body.appendChild(textarea);
textarea.select();
try {
const success = document.execCommand('copy');
document.body.removeChild(textarea);
return success;
} catch {
document.body.removeChild(textarea);
return false;
}
}
// 统一封装
async function safeCopy(text: string) {
if (navigator.clipboard?.writeText) {
try {
await navigator.clipboard.writeText(text);
return true;
} catch {
// 降级
}
}
return fallbackCopyText(text);
}
paste 事件深度处理
拦截与转换粘贴内容
editor.addEventListener('paste', (e) => {
e.preventDefault();
const clipboardData = e.clipboardData!;
// 优先处理 HTML
const html = clipboardData.getData('text/html');
if (html) {
const sanitized = sanitizeHTML(html);
insertHTML(sanitized);
return;
}
// 纯文本
const text = clipboardData.getData('text/plain');
if (text) {
// 检测 URL 自动链接
const linked = text.replace(
/https?:\/\/\S+/g,
(url) => `<a href="${url}">${url}</a>`
);
insertHTML(linked);
}
});
剪贴板数据类型检测
editor.addEventListener('paste', (e) => {
const data = e.clipboardData!;
console.log('类型列表:', [...data.types]);
// 常见类型优先级
const priorityOrder = [
'text/html',
'application/json',
'text/plain',
'image/png',
];
for (const type of priorityOrder) {
if (data.types.includes(type)) {
const content = data.getData(type);
console.log(`粘贴 ${type}:`, content.slice(0, 100));
break;
}
}
});
实战:复制按钮组件
class CopyButton {
private timeoutId: number | null = null;
constructor(private button: HTMLButtonElement, private getText: () => string) {
button.addEventListener('click', () => this.handleCopy());
}
private async handleCopy() {
const text = this.getText();
const success = await safeCopy(text);
if (success) {
this.showFeedback('已复制!');
} else {
this.showFeedback('复制失败');
}
}
private showFeedback(message: string) {
const original = this.button.textContent;
this.button.textContent = message;
this.button.disabled = true;
if (this.timeoutId) clearTimeout(this.timeoutId);
this.timeoutId = window.setTimeout(() => {
this.button.textContent = original;
this.button.disabled = false;
}, 2000);
}
}
浏览器兼容性
| API | Chrome | Firefox | Safari | Edge |
|---|---|---|---|---|
writeText |
66+ | 63+ | 13.1+ | 79+ |
readText |
66+ | 63+ | 13.1+ | 79+ |
write (ClipboardItem) |
76+ | 87+ | 13.1+ | 79+ |
read |
76+ | 87+ | 13.1+ | 79+ |
| Web Custom Formats | 104+ | 不支持 | 不支持 | 104+ |
| 图片剪贴板 | 76+ | 87+ | 13.1+ | 79+ |
最佳实践
- 始终提供降级:
execCommand作为 Clipboard API 不可用时的后备 - 用户激活内调用:复制操作必须在 click/keydown 等事件中触发
- 反馈必做:复制成功/失败都应给出视觉反馈
- 读取需谨慎:
read()涉及隐私,仅在必要时请求 - sanitize 粘贴内容:HTML 粘贴必须过滤 XSS 风险
总结
Async Clipboard API 是现代浏览器剪贴板操作的标准方案,支持文本、富文本、图片和自定义格式的读写。相比 execCommand,它更可靠、更安全、功能更强大,但需注意用户激活要求和权限模型。
本站提供浏览器本地工具,免注册即可试用 →
#Clipboard API#剪贴板#异步API#浏览器安全#用户交互