Back

为代码块创建复制按钮

为代码块创建复制按钮

如果你曾经看过有人费力地手动选中并复制一段代码——或者你自己也经历过——那么你应该明白为什么代码块需要一个复制按钮。这是一个看似微小却能带来真正差异的 UI 细节,尤其是对那些通过键盘、语音命令或触摸屏进行操作的用户来说。

本文将带你使用现代 Clipboard API 构建一个简洁、可靠的代码片段复制 UI——无需任何第三方库。

关键要点

  • 使用 navigator.clipboard.writeText() 实现现代化、基于 Promise 的复制功能,但它需要一个安全上下文(HTTPS 或 localhost)以及由用户发起的事件。
  • 使用 textContent 而非 innerHTML 来提取代码,避免将语法高亮器的 <span> 包装元素与实际代码一同复制。
  • 将调用包裹在 try/catch 块中,以处理权限错误并向用户提供清晰的视觉反馈。
  • 为按钮添加 aria-label 来提升可访问性,尤其是在用图标替代文字标签时。
  • 已弃用的 document.execCommand('copy') 仅应作为遗留环境下的最后兜底方案。

Clipboard API 的 writeText 方法是如何工作的

在 JavaScript 中,将内容复制到剪贴板的现代方式是 navigator.clipboard.writeText()。它基于 Promise,是异步的,并且被所有当前主流浏览器支持。

使用前需要了解两点:

  • 它需要安全上下文。 Clipboard API 只能在 HTTPS 下工作。在 localhost 上,普通 HTTP 也被视为安全的,因此开发时无需担心。
  • 它必须由用户操作触发。click 事件处理函数中调用它,可以自动满足这一要求。
await navigator.clipboard.writeText("your text here");

这就是整个功能的核心。


从代码块中提取文本

你的 HTML 可能长这样:

<pre><code>const greeting = "hello";</code></pre>

要获取原始文本,请使用 textContent——而不是 innerHTML。使用 innerHTML 会把 HTML 标签也一起复制进去,这绝对不是你想要的结果。

const code = document.querySelector("pre code").textContent;

如果你的代码块使用了 Prism.jsHighlight.js 这样的语法高亮器,高亮器会用 <span> 元素包裹各种 token。textContent 会剥离这些标签,只返回纯文本,而这正是应该出现在用户剪贴板中的内容。


为代码块构建复制按钮

下面是一个完整、可用的实现:

document.querySelectorAll("pre").forEach((block) => {
  const button = document.createElement("button");
  button.type = "button";
  button.textContent = "Copy";
  button.setAttribute("aria-label", "Copy code to clipboard");

  button.addEventListener("click", async () => {
    const code = block.querySelector("code")?.textContent ?? "";

    try {
      await navigator.clipboard.writeText(code);
      button.textContent = "Copied!";
      setTimeout(() => (button.textContent = "Copy"), 2000);
    } catch (err) {
      console.error("Copy failed:", err);
      button.textContent = "Failed";
      setTimeout(() => (button.textContent = "Copy"), 2000);
    }
  });

  block.style.position = "relative";
  block.appendChild(button);
});

这里有几点值得注意:

  • aria-label 为按钮提供了一个可被屏幕阅读器识别的可访问名称,当你之后将文字替换为图标时尤为重要。
  • e.currentTargete.target 更安全,特别是当你的按钮包含 SVG 图标等子元素时——e.target 可能会指向图标本身,而不是按钮。(如果需要,可以通过为点击处理函数添加 event 参数来访问它。)
  • try/catch 能够优雅地处理权限拒绝或意外失败,给用户呈现可见的反馈,而不是悄无声息地出错。

旧版浏览器怎么办?

navigator.clipboard 在现代浏览器中获得了极佳的支持。如果你需要兼容旧环境,document.execCommand('copy') 可以作为兜底方案——但它已被弃用且不可靠。请仅将其作为最后的备选,并通过特性检测来使用:

if (!navigator.clipboard) {
  // 使用 document.execCommand('copy') 的兼容方案
}

对于今天大多数文档站点和开发者工具,你完全可以只依赖 Clipboard API。


结语

一个能正常工作的代码块复制按钮归结为三件事:用 textContent 抓取文本,用 navigator.clipboard.writeText() 写入剪贴板,并在成功或失败时给用户明确的反馈。通过 aria-label 保持按钮的可访问性,显式地处理错误,你就能在 30 行以内的纯 JavaScript 中得到一个生产级别的实现。

常见问题

Clipboard API 需要一个安全上下文,这意味着在生产环境中需要 HTTPS。不过,浏览器默认将 localhost 视为安全的,因此开发期间使用普通 HTTP 是没问题的。如果你在像 192.168.x.x 这样的本地网络 IP 上测试,API 会失败,因为那不被视为安全环境。请改用 localhost 或搭建一个 HTTPS 开发服务器。

不可以。Clipboard API 要求用户激活,即调用必须发生在由用户触发的事件处理函数中,如 click、keydown 或 pointerup。在页面加载时或在 setTimeout 中调用 writeText 会静默失败或抛出权限错误。这一限制可以防止恶意网站在未经同意的情况下劫持剪贴板。

textContent 属性已经会原样保留空白字符,包括 HTML 中书写的换行和缩进。只要你的 pre 和 code 元素包含格式正确的源代码,复制出的文本就会与之一致。请避免使用 innerText,因为它会根据 CSS 渲染对空白进行规范化,可能在不同浏览器中产生不一致的结果。

e.target 指的是实际接收到点击的元素,可能是按钮内部的子元素,比如一个 SVG 图标。e.currentTarget 则始终指向事件监听器所附加到的元素,在这里就是按钮本身。使用 currentTarget 可以防止用户点击按钮内嵌套的图标或 span 时出现 bug。

Understand every bug

Uncover frustrations, understand bugs and fix slowdowns like never before with OpenReplay — the open-source session replay tool for developers. Self-host it in minutes, and have complete control over your customer data. Check our GitHub repo and join the thousands of developers in our community.

OpenReplay