React 复制到剪贴板:完整指南
将文本复制到剪贴板听起来很简单,但在 React 中要正确实现它,涉及到浏览器权限、HTTPS 要求和优雅的降级处理。本指南带你回顾 Web 剪贴板访问的演变历程,并展示当前最简洁的处理方式。
旧方法:document.execCommand
在 Clipboard API 出现之前,复制文本意味着创建一个隐藏的 textarea,选中其内容,然后调用 document.execCommand("copy"):
function copyToClipboard(text: string) {
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");
document.body.removeChild(textarea);
}
这种方式有严重的问题。它是同步的,会阻塞主线程。需要创建和移除 DOM 元素。已在所有主流浏览器中被弃用,而且在不同设备上行为不一致,尤其是在 iOS 上。
现代 Clipboard API
浏览器现在提供了 navigator.clipboard,一个基于 Promise 的 API,更简洁、更可靠:
await navigator.clipboard.writeText("Hello, world!");
const text = await navigator.clipboard.readText();
这是正确的基础方案,但在 React 组件中直接使用它会引入一些挑战。
为什么它很棘手
权限
Clipboard API 需要用户明确授权。浏览器可能会在允许读取访问前提示用户,有些浏览器如果调用不是来自用户手势(如点击),会静默拒绝访问。
仅限 HTTPS
navigator.clipboard 仅在安全上下文中可用。如果你的应用在开发时运行在 http://localhost 上是没问题的,但任何部署的站点都必须使用 HTTPS。
SSR 和 Server Components
navigator.clipboard 在服务端不存在。如果你使用 Next.js、Remix 或任何 SSR 框架,在模块层级引用它会抛出 ReferenceError。
降级和错误处理
你需要处理 API 不可用、用户拒绝权限或文档未获得焦点的情况。每次需要复制按钮时都要写大量的防御性代码。
useClipboard 来拯救
ReactUse 的 useClipboard Hook 将所有这些复杂性封装在一个简单的二元组中:
import { useClipboard } from "@reactuses/core";
function App() {
const [clipboardText, copy] = useClipboard();
return (
<div>
<p>Current clipboard: {clipboardText}</p>
<button onClick={() => copy("Copied with useClipboard!")}>
Copy Text
</button>
</div>
);
}
该 Hook 返回:
clipboardText— 剪贴板的当前内容,在复制、剪切和焦点事件时自动更新。copy(text)— 一个将文本写入剪贴板的异步函数。
在底层,useClipboard 监听 copy、cut 和 focus 事件,使显示的剪贴板值保持同步。它还会在文档未获得焦点时阻止读取剪贴板,否则在大多数浏览器中会抛出错误。
构建带反馈的复制按钮
用户需要视觉确认复制操作是否成功。这里有一个可复用的组件,会短暂显示”已复制!“消息:
import { useState } from "react";
import { useClipboard } from "@reactuses/core";
function CopyButton({ text }: { text: string }) {
const [, copy] = useClipboard();
const [copied, setCopied] = useState(false);
const handleCopy = async () => {
try {
await copy(text);
setCopied(true);
setTimeout(() => setCopied(false), 2000);
} catch {
console.error("Failed to copy");
}
};
return (
<button onClick={handleCopy}>
{copied ? "Copied!" : "Copy"}
</button>
);
}
因为 copy 返回一个 Promise,你可以捕获错误并为成功和失败状态提供反馈。
常见使用场景
代码块
为语法高亮的代码块添加复制按钮,让读者无需手动选择文本即可获取代码片段:
<div style={{ position: "relative" }}>
<pre><code>{codeSnippet}</code></pre>
<CopyButton text={codeSnippet} />
</div>
分享链接
让用户一键复制可分享的 URL,而不必依赖浏览器的地址栏:
<CopyButton text={`https://myapp.com/post/${postId}`} />
表单值
直接从表单字段复制生成的值,如 API 密钥、邀请码或配置字符串:
function ApiKeyField({ apiKey }: { apiKey: string }) {
const [, copy] = useClipboard();
return (
<div>
<input readOnly value={apiKey} />
<button onClick={() => copy(apiKey)}>Copy Key</button>
</div>
);
}
安装
npm i @reactuses/core
然后导入 Hook:
import { useClipboard } from "@reactuses/core";
相关 Hooks
- useClipboard 文档 — 完整 API 参考和在线演示
- usePermission — 检查剪贴板读取权限是否已被授予
- useEventListener — useClipboard 内部使用的底层 Hook
- useSupported — 检测当前环境中 Clipboard API 是否可用
ReactUse 提供了 100 多个 React Hooks。查看全部 →