12k
All articles

为代码块创建复制按钮

使用 Clipboard API 为代码块添加复制按钮,结合 textContent、try/catch、视觉反馈和可访问的 aria-label。

OpenReplay Team
OpenReplay Team
为代码块创建复制按钮

如果你曾经看过有人费力地手动选中并复制一段代码——或者你自己也经历过——那么你应该明白为什么代码块需要一个复制按钮。这是一个看似微小却能带来真正差异的 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 在我的本地开发服务器上不工作?

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 和 e.currentTarget 有什么区别?

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

Understand every bug

Uncover frustrations, understand bugs and fix slowdowns like never before with OpenReplay — self-hosted, with full data ownership.

Star on GitHub

We use cookies to improve your experience. By using our site, you accept cookies.