复制粘贴看起来是个小功能,但真要在React项目里做到全浏览器兼容,坑其实不少。navigator.clipboard这套新API体验最好,却是异步的且要求安全上下文;老方法document.execCommand('copy')虽然被标记为废弃,胜在兼容老浏览器。本文把两套方案的实现细节、限制条件和封装方式讲清楚,最后给出一个可以直接复用的自定义Hook。

一、为什么会有两套剪贴板方案
历史上,网页读写剪贴板只有一条路:先借助<textarea>或<input>承接内容,调用select()选中文本,再执行document.execCommand('copy')。这套流程依赖真实的DOM节点和选区,操作繁琐,而且execCommand整个API已被W3C标记为过时,未来版本随时可能移除。
为了取代它,浏览器推出了异步的Clipboard API,挂在navigator.clipboard下,提供writeText、readText以及更强大的write、read方法,支持复制富文本和图片。它的调用不依赖DOM选区,语法是Promise风格的,和React的函数式写法天然契合。
但新API有一个硬性门槛:只在安全上下文(HTTPS或localhost)中可用。如果你的页面跑在HTTP环境,navigator.clipboard直接是undefined。这就是为什么生产项目中往往两套方案都要写,用Clipboard API做首选,execCommand兜底。
二、Clipboard API的正确用法
写操作最常用的是navigator.clipboard.writeText,返回Promise。有一点要特别注意:它必须在用户手势触发的调用栈中执行,比如点击事件回调里。如果你先await一个网络请求再去复制,可能会报NotAllowedError,因为用户手势的上下文已经丢失。遇到这种情况,可以先复制占位文本,或者提示用户再次点击。
读取剪贴板用readText,权限要求更严格。Chrome会弹权限询问,某些环境下还会静默失败。所以在React中最好用try/catch把错误接住,给用户明确提示,而不是让Promise静默reject。
async function copyToClipboard(text) {
if (!navigator.clipboard || !window.isSecureContext) {
throw new Error('当前环境不支持 Clipboard API');
}
await navigator.clipboard.writeText(text);
}
// 在React组件中使用
function CopyButton({ text }) {
const handleCopy = async () => {
try {
await copyToClipboard(text);
alert('复制成功');
} catch (err) {
console.error(err);
alert('复制失败,请手动复制');
}
};
return <button onClick={handleCopy}>复制</button>;
}如果需要复制图片或富文本,就要用navigator.clipboard.write配合ClipboardItem,把内容包装成Blob。注意Safari对ClipboardItem的构造参数有特殊要求,接受一个返回Promise的对象,这样可以在用户点击时才真正生成Blob,绕过手势丢失问题。
三、execCommand降级方案与完整的兼容Hook
execCommand的复制套路是固定的:创建一个临时<textarea>,设置value后加入文档,调用select(),执行document.execCommand('copy'),最后移除节点。有两个细节容易踩坑:一是要把textarea定位到屏幕外而不是display:none,否则部分浏览器无法选中;二是iOS Safari需要手动调用setSelectionRange并且contentEditable要设为true才能正常工作。
把两套方案合到一起,封装成一个useCopyHook,组件里调用就非常干净。下面是完整实现:
import { useCallback, useState } from 'react';
function legacyCopy(text) {
const textarea = document.createElement('textarea');
textarea.value = text;
textarea.style.position = 'fixed';
textarea.style.top = '-9999px';
textarea.setAttribute('readonly', '');
document.body.appendChild(textarea);
if (navigator.userAgent.match(/ipad|iphone/i)) {
textarea.contentEditable = true;
textarea.readOnly = false;
const range = document.createRange();
range.selectNodeContents(textarea);
const selection = window.getSelection();
selection.removeAllRanges();
selection.addRange(range);
textarea.setSelectionRange(0, text.length);
} else {
textarea.select();
}
const ok = document.execCommand('copy');
document.body.removeChild(textarea);
return ok;
}
export function useCopy() {
const [copied, setCopied] = useState(false);
const copy = useCallback(async (text) => {
let ok = false;
if (navigator.clipboard && window.isSecureContext) {
try {
await navigator.clipboard.writeText(text);
ok = true;
} catch {
ok = legacyCopy(text);
}
} else {
ok = legacyCopy(text);
}
if (ok) {
setCopied(true);
setTimeout(() => setCopied(false), 2000);
}
return ok;
}, []);
return [copied, copy];
}组件里使用时,把copied状态用来切换按钮文案,复制成功后两秒内显示已复制,体验上比alert友好得多。下面是一个简单的调用示例:
import { useCopy } from './useCopy';
function InviteCode({ code }) {
const [copied, copy] = useCopy();
return (
<button onClick={() => copy(code)}>
{copied ? '已复制' : '复制邀请码'}
</button>
);
}另外提醒一点:粘贴功能(读取剪贴板)没有可靠的execCommand降级,如果要兼容HTTP环境,只能引导用户使用Ctrl+V配合onPaste事件来获取内容,这也是各大富文本编辑器的通用做法。掌握这套组合方案后,无论是复制邀请码、分享链接还是读取粘贴内容,都能在React项目中稳定实现。
React复制粘贴Clipboard APIdocument.execCommand修改时间:2026-09-15 03:49:25