“点击复制”是工具页和代码块上最不起眼、也最容易做错的功能。做错的常见姿势是:只处理了成功。
三条铁律
navigator.clipboard.writeText() 只在以下前提成立时存在且可用:
- 安全上下文:HTTPS 或
localhost。http://局域网 IP 访问时navigator.clipboard干脆是undefined。 - 文档获得过用户手势:多数浏览器要求调用栈可以追溯到一次真实点击/按键,脚本自动触发会被拒绝。
- 权限与焦点:页面不可见或用户在权限策略中拒绝了剪贴板写入时,Promise 以
NotAllowedError拒绝。
任何一条不满足,clipboard API 路径就走不通——所以复制功能必须有“失败也要被说出来”的设计。
失败反馈是最常被遗漏的分支
一个真实做对的复制按钮应当区分三种结果:
const copyText = async (text, button) => {
const label = button.querySelector('[data-label]');
const original = label.textContent;
try {
if (!navigator.clipboard?.writeText) {
throw new Error('insecure-context');
}
await navigator.clipboard.writeText(text);
label.textContent = '已复制';
button.classList.add('is-copied');
} catch {
// 关键分支:不假装成功
label.textContent = '复制失败,请手动选择复制';
} finally {
window.setTimeout(() => {
label.textContent = original;
button.classList.remove('is-copied');
}, 1600);
}
};
两个细节:
is-copied的视觉反馈只在成功路径加上;失败时用文案差异即可,不要让失败看起来像成功。- 定时器要记账。若页面跑在客户端路由下(脚本体会重新执行),保存这些 timeout 并在换页前清除,否则残留回调会写进已经卸载的 DOM——这是动效批次的复盘教训在复制组件上的重演。
旧世界:execCommand 与 textarea 兜底
document.execCommand('copy') 早已废弃,且在现代浏览器中同样受权限约束,只是报错方式更暧昧(返回 false)。它唯一还值得存在的理由是非安全上下文的兜底:
const legacyCopy = (text) => {
const area = document.createElement('textarea');
area.value = text;
area.setAttribute('readonly', '');
area.style.position = 'fixed';
area.style.opacity = '0';
document.body.append(area);
area.select();
const ok = document.execCommand('copy');
area.remove();
return ok;
};
先试 async API、失败再落 legacyCopy、两条路都不通才提示手动复制——三层漏斗能让绝大多数用户拿到内容,同时保留诚实的兜底话术。
读剪贴板是另一回事
readText() 的权限模型严格得多(通常要求用户显式手势 + 权限弹窗),粘贴类工具(JSON 校验器、diff 对比器)应当提供“手动粘贴”输入区作为一等公民,把“一键读取剪贴板”当作锦上添花的快捷路径,而不是唯一入口。
反馈动效的克制
复制成功的动画(短脉冲、像素星星之类)适可而止:一次性、≤1 秒、可被 prefers-reduced-motion 完全关闭,关闭后文本反馈“已复制”依然在场。动画是强调,不是信息载体本身。
小结
剪贴板功能的质量不在“能不能复制”,而在不能复制的那一刻你说了什么。给失败一个准确、可操作的出口(选中手动复制的提示),比在成功时多放一颗星星更能建立信任。