你刚点完"复制密钥",看到绿色对勾,自信地切到终端粘贴——结果粘进来的是昨天复制的快递单号。这不是用户手滑,是代码在假装成功。

我在给管理后台加"复制令牌"按钮时撞上了这个坑。剪贴板接口(Clipboard API)返回了 undefined,控制台却干干净净。用户会看见勾选动画,粘贴时却一片空白,甚至更糟:粘上的是剪贴板里残留的任意内容。在那个场景下,"任意内容"可能是任何东西。

打开网易新闻 查看精彩图片

这让我想起 Hacker News 上那篇 977 赞的热帖《Copy Fail》。作者扒了一个真实现象:navigator.clipboard.writeText() 会在特定场景下静默失败。没有异常,没有拒绝回调,什么都没有。用户点击,图标变勾,剪贴板原封不动。

帖子的评论区炸了,因为谁都见过这毛病,但没人说得清根因。答案从"Chromium 的权限模型"到"iframe 的锅"到"文档没聚焦",个个都对,个个不全。

我的判断:问题不是剪贴板会失败,而是我们写的交互假设它永远不会失败。这个假设在复制密码、API 密钥、私钥时尤其危险。

我在自己的环境里复现了这个问题。这是发现。

三个场景,三种死法

我拆了一个生产环境跑着的组件——管理后台里复制 API 密钥的按钮。技术栈:Next.js 15、TypeScript、Railway 托管、反向代理后面。

第一个意外:bug 不是在哪都复现。我搭了三个场景才摸清规律。

场景一,iframe 埋雷。组件如果套在 iframe 里,且没写 allow="clipboard-write" 属性,writeText() 的 Promise 会 resolve,但什么都不写。我的 setCopied(true) 照跑,用户看见绿勾。

场景二,权限查询的幻觉。我以为用 Permissions API 先查 clipboard-write 状态就能避险。结果在部分浏览器里,query() 返回的 state 是"prompt"——不是"granted"也不是"denied"。这时候调用 writeText(),用户可能根本没看到权限弹窗,Promise 就已经挂了。

场景三,最阴的。文档失去焦点时(比如用户点了按钮后快速切到别的标签页),writeText() 的行为因浏览器而异。Chrome 可能静默失败,Firefox 可能抛异常,Safari 可能直接忽略调用

同一行代码,三种尸体。

为什么错误抓不到

Clipboard API 的设计是 Promise 化的,这让我们误以为能用 try-catch 兜底。但规范里留了一个后门:如果权限检查在底层就被拦截,Promise 可以 resolve 为 undefined,而不是 reject。

我的日志证实了这点。在 iframe 场景里,writeText() 返回的 Promise 正常走完,await 之后我拿到的是 undefined,不是抛错。我的 catch 块成了摆设。

更麻烦的是,浏览器的权限模型和剪贴板 API 是两套系统。Permissions API 能查到的"clipboard-write",和 writeText() 实际需要的权限,在边缘场景下并不完全对齐。查出来"可以写",不代表真能写。

我试了一种防御写法:先读剪贴板,再写,再读验证。结果撞上了另一个限制——readText() 需要"clipboard-read"权限,比 write 更严格。这套方案在 HTTP 环境下直接爆炸,因为剪贴板 API 要求安全上下文(secure context)。

fallback 不是退路,是主路

我最终保留了两套方案。第一套走 Clipboard API,但加了显式的权限预检和结果验证。第二套是 document.execCommand('copy'),这个被标记为废弃的老 API,在 iframe 和旧浏览器里反而更稳。

execCommand 的缺陷很明显:它只能写选中的文本,所以我需要临时创建一个 textarea,塞入内容,选中,执行复制,再清理 DOM。代码丑,但行为可预测。失败时它会返回 false,而不是假装成功。

关键改动在 UX 层。我不再假设"调用复制 = 复制成功"。无论走哪条路,最后都验证一次:尝试读剪贴板(在允许读的上下文里),或者至少确保 Promise 返回的不是 undefined。只有验证通过,才显示那个绿色对勾。

代价是延迟。验证多了几十毫秒,但相比让用户把旧密钥粘贴到生产环境,这买卖划算。

这件事的实用指向

剪贴板 API 的静默失败是个设计债,但短期内不会改。规范允许 Promise resolve 为空,浏览器厂商各有各的实现偏差,你等不及他们统一。

能做的是三件事:一,复制敏感内容时,永远准备 fallback 方案,execCommand 虽然 deprecated,但死得比你的用户信任慢;二,别信 Promise 的 resolve,显式检查结果或者做写后验证;三,把"复制成功"的反馈从"我调用了 API"改成"我确认了内容在剪贴板里"。

那个绿色对勾,用户已经学会无视了。你要给的是他们敢切到终端直接粘贴的信心。