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 监听 copycutfocus 事件,使显示的剪贴板值保持同步。它还会在文档未获得焦点时阻止读取剪贴板,否则在大多数浏览器中会抛出错误。

构建带反馈的复制按钮

用户需要视觉确认复制操作是否成功。这里有一个可复用的组件,会短暂显示”已复制!“消息:

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


ReactUse 提供了 100 多个 React Hooks。查看全部 →