几乎所有 Web 应用都有「复制到剪贴板」功能——复制链接、复制代码、复制 API Key。但你真的了解剪贴板 API 吗?document.execCommand('copy') 早已被标记为废弃(deprecated),而现代的异步 Clipboard API 不仅能处理纯文本,还能读写富文本(HTML)、图片甚至自定义 MIME 类型。据统计,超过 70% 的前端项目仍在使用已废弃的剪贴板方案,本文将系统讲解现代 Clipboard API 的完整用法,帮你彻底告别 execCommand 的历史包袱。
🔧 一、从 execCommand 到异步 Clipboard API
1.1 旧方案的问题
在 Clipboard API 出现之前,操作剪贴板全靠 document.execCommand。这个 API 有三个致命缺陷:
- ❌ 同步阻塞:在主线程上执行,大量数据时可能卡顿
- ❌ 安全限制多:必须由用户手势(click/keydown)触发,且不能在后台静默执行
- ❌ 只能写不能读:
execCommand('paste')在大多数浏览器中被直接禁用
// ❌ 已废弃的旧方案 — 不要在新项目中使用
function oldCopy(text) {
const textarea = document.createElement('textarea');
textarea.value = text;
textarea.style.position = 'fixed';
textarea.style.opacity = '0';
document.body.appendChild(textarea);
textarea.select();
document.execCommand('copy'); // ⚠️ deprecated
document.body.removeChild(textarea);
}
1.2 现代 Clipboard API 基础
现代 Clipboard API 基于 navigator.clipboard 对象,提供 readText()、writeText()、read()、write() 四个核心方法,全部返回 Promise。
// ✅ 现代方案:异步写入纯文本
async function copyText(text) {
try {
await navigator.clipboard.writeText(text);
console.log('复制成功');
} catch (err) {
console.error('复制失败:', err);
}
}
// ✅ 现代方案:异步读取纯文本
async function pasteText() {
try {
const text = await navigator.clipboard.readText();
console.log('剪贴板内容:', text);
} catch (err) {
console.error('读取失败:', err);
}
}
⚠️ 警告:
readText()和read()需要用户明确授予权限。在 Firefox 中,读取剪贴板会弹出权限提示栏,用户必须点击「允许」才能继续。
1.3 权限模型与安全策略
Clipboard API 的安全模型比 execCommand 更严格,但也更清晰:
| 操作 | writeText |
readText / read / write |
|---|---|---|
| 用户手势要求 | ✅ 推荐(Chrome 隐式要求) | ✅ 必须 |
| HTTPS 要求 | ✅ 必须 | ✅ 必须 |
| 权限弹窗 | ❌ 不弹 | ✅ Firefox 弹窗 |
| iframe 中使用 | ⚠️ 需 allow="clipboard-write" |
⚠️ 需 allow="clipboard-read" |
| Service Worker | ❌ 不可用 | ❌ 不可用 |
📌 **记住:**Clipboard API 在非安全上下文(HTTP)下完全不可用。如果你的开发环境用的是
http://localhost,Chrome 会放行,但其他域名的 HTTP 请求会被直接拒绝。
🚀 二、富文本与图片的剪贴板操作
纯文本复制只是冰山一角。真正强大的是 Clipboard API 对多 MIME 类型的支持——你可以往剪贴板写入 HTML 富文本、PNG 图片、SVG 甚至自定义格式。
2.1 读写富文本(HTML)
剪贴板内部以 Blob 数组存储数据,每个 Blob 对应一个 MIME 类型。写入 HTML 时,需要同时提供 text/html 和 text/plain 两种格式,确保在不支持富文本的目标应用中也能粘贴纯文本。
// ✅ 同时写入 HTML 和纯文本(降级兼容)
async function copyRichText(html, plainText) {
const htmlBlob = new Blob([html], { type: 'text/html' });
const textBlob = new Blob([plainText], { type: 'text/plain' });
const item = new ClipboardItem({
'text/html': htmlBlob,
'text/plain': textBlob,
});
await navigator.clipboard.write([item]);
}
// 使用示例
copyRichText(
'<h1 style="color: #2563eb;">Hello</h1><p>这是一段<strong>富文本</strong></p>',
'Hello\n这是一段富文本'
);
2.2 读取剪贴板中的 HTML
read() 方法返回 ClipboardItem 数组,你需要遍历 types 来判断剪贴板中有哪些格式可用:
// ✅ 读取剪贴板,优先取 HTML,降级取纯文本
async function readFromClipboard() {
try {
const items = await navigator.clipboard.read();
for (const item of items) {
// 优先读取 HTML
if (item.types.includes('text/html')) {
const htmlBlob = await item.getType('text/html');
const html = await htmlBlob.text();
return { type: 'html', content: html };
}
// 降级读取纯文本
if (item.types.includes('text/plain')) {
const textBlob = await item.getType('text/plain');
const text = await textBlob.text();
return { type: 'text', content: text };
}
}
return { type: 'empty', content: '' };
} catch (err) {
console.error('读取剪贴板失败:', err);
return { type: 'error', content: err.message };
}
}
2.3 图片剪贴板操作
这是 Clipboard API 最实用的高级功能之一——你可以直接将图片复制到剪贴板,或从剪贴板粘贴图片。这在截图工具、在线图片编辑器、富文本编辑器中非常有用。
// ✅ 将 Canvas 内容复制到剪贴板
async function copyCanvasToClipboard(canvas) {
// canvas.toBlob() 将 Canvas 转为 PNG Blob
const blob = await new Promise(resolve => {
canvas.toBlob(resolve, 'image/png');
});
const item = new ClipboardItem({ 'image/png': blob });
await navigator.clipboard.write([item]);
}
// ✅ 从剪贴板读取图片(例如用户 Ctrl+V 粘贴截图)
async function readImageFromClipboard() {
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);
// 转为可在 <img> 中使用的 Object URL
const url = URL.createObjectURL(blob);
return { url, blob, type };
}
}
}
return null;
}
💡 提示:
ClipboardItem构造函数也接受一个{ presentationStyle: 'inline' }选项,表示粘贴时内容应内联显示(而非作为附件)。这对富文本编辑器很有用。
💡 三、生产环境中的实用模式
3.1 兼容性封装:统一新旧 API
虽然现代浏览器都支持 Clipboard API,但你仍然需要兼容旧版浏览器(如 IE11、旧版 Android WebView)。以下是一个生产级的封装方案:
// ✅ 生产级剪贴板封装 — 自动降级
const clipboard = {
async writeText(text) {
// 优先使用现代 API
if (navigator.clipboard?.writeText) {
try {
await navigator.clipboard.writeText(text);
return true;
} catch {
// 权限被拒绝时降级
}
}
// 降级方案:execCommand
return this._fallbackCopy(text);
},
_fallbackCopy(text) {
const textarea = document.createElement('textarea');
textarea.value = text;
textarea.style.cssText = 'position:fixed;left:-9999px;opacity:0';
document.body.appendChild(textarea);
textarea.select();
try {
document.execCommand('copy');
return true;
} catch {
return false;
} finally {
document.body.removeChild(textarea);
}
},
async readText() {
if (navigator.clipboard?.readText) {
try {
return await navigator.clipboard.readText();
} catch {
return null;
}
}
return null; // 旧浏览器无法静默读取
},
};
3.2 「复制按钮」的最佳 UX 模式
复制按钮是开发者工具站的标配功能。一个优秀的复制按钮需要处理三个状态:空闲 → 复制中 → 已复制。以下是结合 Vue 3 Composition API 的完整实现:
// ✅ Vue 3 复制按钮 composable
import { ref } from 'vue';
export function useCopy(timeout = 2000) {
const copied = ref(false);
const error = ref(null);
let timer = null;
async function copy(text) {
error.value = null;
try {
await navigator.clipboard.writeText(text);
copied.value = true;
clearTimeout(timer);
timer = setTimeout(() => { copied.value = false; }, timeout);
} catch (err) {
error.value = err.message;
copied.value = false;
}
}
return { copied, error, copy };
}
// 在组件中使用
// <button @click="copy('要复制的内容')">
// {{ copied ? '✅ 已复制' : '📋 复制' }}
// </button>
3.3 处理 paste 事件:图片上传场景
在富文本编辑器或聊天应用中,用户经常直接 Ctrl+V 粘贴截图。监听 paste 事件比使用 navigator.clipboard.read() 更可靠,因为 read() 在部分浏览器中需要额外权限:
// ✅ 监听粘贴事件,提取图片
function setupPasteHandler(onImagePasted) {
document.addEventListener('paste', async (event) => {
const items = event.clipboardData?.items;
if (!items) return;
for (const item of items) {
// 只处理图片类型
if (item.type.startsWith('image/')) {
event.preventDefault();
const file = item.getAsFile();
if (!file) continue;
// 生成预览 URL
const previewUrl = URL.createObjectURL(file);
onImagePasted({ file, previewUrl, type: item.type });
break; // 只取第一张图片
}
}
});
}
// 使用
setupPasteHandler(({ file, previewUrl }) => {
console.log('用户粘贴了图片:', file.name, file.size);
// 上传到服务器或显示预览
});
📌 记住:
paste事件中的clipboardData是DataTransfer对象,与ClipboardItem的 API 不同。getAsFile()方法返回File对象,可以直接用于FormData上传。
3.4 自定义 MIME 类型:跨应用数据传递
Clipboard API 支持自定义 MIME 类型(以 web 前缀开头),可以在同一浏览器的不同标签页之间传递结构化数据:
// ✅ 写入自定义格式
async function copyCustomData(data) {
const jsonBlob = new Blob([JSON.stringify(data)], {
type: 'application/json',
});
const item = new ClipboardItem({
'application/json': jsonBlob,
'text/plain': new Blob([data.text || ''], { type: 'text/plain' }),
});
await navigator.clipboard.write([item]);
}
// ✅ 读取自定义格式
async function readCustomData() {
const items = await navigator.clipboard.read();
for (const item of items) {
if (item.types.includes('application/json')) {
const blob = await item.getType('application/json');
return JSON.parse(await blob.text());
}
}
return null;
}
⚠️ 四、跨浏览器兼容性与踩坑指南
4.1 浏览器支持情况
| API | Chrome | Firefox | Safari | Edge |
|---|---|---|---|---|
writeText() |
✅ 66+ | ✅ 63+ | ✅ 13.1+ | ✅ 79+ |
readText() |
✅ 76+ | ✅ 63+(需权限) | ✅ 13.1+ | ✅ 79+ |
write() (富文本/图片) |
✅ 76+ | ✅ 87+ | ✅ 13.1+ | ✅ 79+ |
read() (富文本/图片) |
✅ 76+ | ✅ 87+(需权限) | ⚠️ 部分支持 | ✅ 79+ |
ClipboardItem |
✅ 76+ | ✅ 87+ | ✅ 13.1+ | ✅ 79+ |
4.2 常见踩坑点
坑点 1:Chrome 的「短暂权限」
在 Chrome 中,writeText() 不需要显式请求权限,但 readText() 需要。更坑的是,用户点击「拒绝」后,后续的 readText() 调用会直接抛出 NotAllowedError,且无法通过代码再次弹出权限提示——用户必须手动在浏览器设置中修改。
坑点 2:Safari 的图片读取限制
Safari 13.1+ 支持 write() 写入图片,但 read() 读取图片在某些版本中会静默失败。建议在 Safari 中通过 paste 事件来读取图片。
坑点 3:Firefox 的权限提示栏
Firefox 对 readText() 和 read() 都会弹出权限提示栏。这在自动化测试中是巨大的障碍。如果你的应用在测试中需要操作剪贴板,建议使用 Playwright 的 page.evaluate() 配合 document.execCommand 作为测试专用降级。
坑点 4:iframe 中的权限隔离
在 iframe 中使用 Clipboard API 需要在 <iframe> 标签上添加 allow="clipboard-read; clipboard-write" 属性,否则会抛出 NotAllowedError。
<!-- ✅ 允许 iframe 中使用剪贴板 API -->
<iframe
src="https://example.com/editor"
allow="clipboard-read; clipboard-write"
></iframe>
📊 五、方案对比与选型建议
如果你的项目需要剪贴板功能,以下是主流方案的对比:
| 特性 | 自己封装 Clipboard API | clipboard.js (npm 包) | useClipboard (VueUse) |
|---|---|---|---|
| 包大小 | 0 KB | ~3 KB gzip | ~1 KB gzip |
| 富文本支持 | ✅ | ❌ 仅纯文本 | ❌ 仅纯文本 |
| 图片支持 | ✅ | ❌ | ❌ |
| 旧浏览器兼容 | 需手动降级 | 自动降级 | 自动降级 |
| TypeScript 支持 | 自己写 | ✅ 内置 | ✅ 内置 |
| 维护状态 | — | ⚠️ 不太活跃 | ✅ 活跃 |
⚡ **关键结论:**如果你只需要纯文本复制,直接用 VueUse 的 useClipboard 或 clipboard.js 即可。但如果你需要富文本或图片操作,必须直接使用原生 Clipboard API——没有任何第三方库封装了这些高级功能。
✅ 总结与最佳实践
以下是使用 Clipboard API 的核心建议:
- ✅ 新项目直接用
navigator.clipboard,不要再用execCommand - ✅ 写入时同时提供 HTML + 纯文本,确保降级兼容
- ✅ 读取图片优先用
paste事件,比clipboard.read()兼容性更好 - ✅ 封装时保留
execCommand降级路径,覆盖旧浏览器和 WebView - ❌ 不要在页面加载时就读取剪贴板,这会被浏览器拦截并报错
- ❌ 不要假设剪贴板中有特定格式,必须先检查
item.types - ⚠️ 注意 Safari 的图片读取限制,做好 Feature Detection
⚡ **关键结论:**剪贴板 API 看似简单,实际涉及权限模型、MIME 类型、浏览器差异等多个层面。掌握
read()/write()的多格式操作能力,是构建高质量 Web 工具(如在线代码编辑器、截图标注工具、富文本编辑器)的基础功。
相关工具推荐:
- 🔧 jsjson.com JSON 格式化工具 — 配合剪贴板复制格式化后的 JSON
- 🔧 jsjson.com 代码转换工具 — 复制粘贴代码格式转换
- 📦 VueUse useClipboard — Vue 3 项目首选
- 📦 clipboard.js — 轻量纯文本复制