剪贴板操作 Clipboard API 教程:原文核对与安全修订

原作者:阮一峰(2021-01-20);授权核查与编辑整理:未完纪。

六个模块串起用户点击、异步 Clipboard API、ClipboardItem 格式、复制剪切事件、粘贴事件与安全输出,并强调不应静默读取剪贴板。
异步接口与剪贴板事件的职责及数据边界(自绘示意图,非截图。)

一键复制链接、在编辑器里粘贴文字、把图片复制到别的应用,背后都涉及系统剪贴板。网页能够读写它,并不代表网页应当随意改变它。阮一峰在原文开头强调:剪贴板操作应该符合用户预期;一个明确标注用途的复制按钮通常比在页面任意点击时自动读写更合适。

本文依据阮一峰 2021 年 1 月 20 日的《剪贴板操作 Clipboard API 教程》完整正文重新整理,保留传统命令、异步接口与剪贴板事件三条路线。浏览器限制按核验时的 MDN Clipboard API 文档 补充;代码错误和编辑改写都在对应位置说明。这里没有读取用户剪贴板,也没有运行示例或测试任何浏览器。

三种方式分别适合什么情况

方式 数据从哪里来 适用边界
document.execCommand() 当前被选中的文本或有焦点的编辑区 传统同步接口,现已弃用;尤其 paste 不能作为跨浏览器保证。
navigator.clipboard 系统剪贴板,或应用准备写入的数据 异步 Promise API,受安全上下文、权限、用户激活及焦点等条件约束。
copy / cut / paste 事件 用户正在进行的复制、剪切或粘贴操作 在事件处理器内使用 event.clipboardData,可定制这次操作。

传统命令:先选中文本,再请求复制

原文先介绍 document.execCommand("copy")、"cut" 和 "paste"。复制时选中 input 或 textarea 的内容,调用 copy 后,浏览器尝试把选区写入剪贴板。下面保留复制的历史用法,放在明确的按钮点击回调里:

copyButton.addEventListener('click', () => {
  const input = document.querySelector('#input');
  input.select();
  const copied = document.execCommand('copy');
  status.textContent = copied ? '已复制' : '请手动选择文字并复制';
});

原文的粘贴示例先对输出元素调用 focus(),再执行 document.execCommand("paste")。这属于历史说明,不应据此承诺普通网页能在现代浏览器中随意粘贴。该接口已经弃用,支持与权限限制因环境而异。它还依赖选区、焦点和同步调用,复制大量内容可能影响页面响应。新增功能应优先使用异步 API,并给用户保留手动复制粘贴方式。

异步 Clipboard API 的入口与安全限制

navigator.clipboard 返回 Clipboard 对象,其 readText、read、writeText、write 方法都返回 Promise。异步避免把等待过程写成同步阻塞,但并不意味着任意格式、任意尺寸的数据都受支持,或所有后续处理都不会占用主线程。

原文将 navigator.clipboard 不存在解释为浏览器不支持。更完整的判断还包括页面是否处于安全上下文:HTTPS 是常见要求,本地开发的可信 localhost 环境可有例外。应结合 window.isSecureContext 与功能检测,再处理调用失败。

if (!window.isSecureContext || !navigator.clipboard?.writeText) {
  status.textContent = '此页面不能自动复制,请手动复制。';
}

原文按当时 Chrome 的行为解释 clipboard-read 与 clipboard-write 权限,并写到写权限可自动获得。这个描述不能当作所有浏览器的统一契约。MDN 当前说明 Chromium 的写入需要权限或短暂用户激活,读取还涉及焦点与权限;Firefox 和 Safari 的写入需要短暂用户激活,读取可能出现专门的粘贴提示,而它们不支持同样的 clipboard-read、clipboard-write Permissions API 权限名称。iframe 还受相关 Permissions-Policy 限制。

因此不要在页面加载、任意点击或定时器中静默读取剪贴板。将读写绑定到说明清楚的按钮,捕获 NotAllowedError 等失败,提示用户用系统快捷键完成操作。原文用 setTimeout 延迟读取并快速切回网页来解释开发者工具焦点问题;延时不会创造权限,也不保证保留短暂用户激活,不能把它作为兼容性方案。剪贴板属于操作系统,而非某一个网页的私有缓冲区。

读写纯文本:readText 与 writeText

readText() 读取剪贴板中的文本;writeText(text) 把文本写入剪贴板。原文示例把监听器放在 document.body,并把读取结果打印到控制台。下面是编辑改写:只在指定按钮点击时操作,结果写入文本框,失败时显示提示,不把可能包含密码或令牌的内容写入日志。假定页面已有 pasteButton、copyButton、output 和 status 对应的元素引用。

pasteButton.addEventListener('click', async () => {
  try {
    if (!navigator.clipboard?.readText) {
      throw new Error('Clipboard readText unavailable');
    }
    output.value = await navigator.clipboard.readText();
    status.textContent = '已粘贴文本';
  } catch {
    status.textContent = '无法自动读取,请在输入框中手动粘贴。';
  }
});

copyButton.addEventListener('click', async () => {
  try {
    if (!navigator.clipboard?.writeText) {
      throw new Error('Clipboard writeText unavailable');
    }
    await navigator.clipboard.writeText(location.href);
    status.textContent = '已复制页面链接';
  } catch {
    status.textContent = '复制失败,请手动复制页面链接。';
  }
});

成功反馈要放在 await 之后。写入被拒绝时,页面若先显示“复制成功”,用户可能误以为剪贴板已更新。写入页面 URL 也应确认链接里没有不应共享的临时令牌或敏感查询参数。读取的文本放入 value 或 textContent,不能未经清理写入 innerHTML,否则剪贴板中的 HTML 可能成为注入入口。

读取多种格式:read 与 ClipboardItem

read() 的结果是 ClipboardItem 数组。每个 item 可以包含多种表示,item.types 列出 MIME 类型,item.getType(type) 返回该表示的 Blob Promise。同一内容可能同时提供 text/plain 与 text/html,调用者选择自己确实需要的类型即可。

下面保留原文逐项取数据的方式,限定只处理 PNG。编辑补充对象 URL 的释放,避免反复预览时持有不必要的资源;它不把剪贴板内容上传到服务器。read 按钮、preview 图片与 status 文本元素由页面提供。

let previewURL;
readImageButton.addEventListener('click', async () => {
  try {
    const items = await navigator.clipboard.read();
    for (const item of items) {
      if (!item.types.includes('image/png')) continue;
      const blob = await item.getType('image/png');
      if (previewURL) URL.revokeObjectURL(previewURL);
      previewURL = URL.createObjectURL(blob);
      preview.src = previewURL;
      status.textContent = '已读取 PNG 图片';
      return;
    }
    status.textContent = '剪贴板没有可用的 PNG 图片';
  } catch {
    status.textContent = '无法读取图片,请手动粘贴或选择本地文件。';
  }
});
window.addEventListener('pagehide', () => {
  if (previewURL) URL.revokeObjectURL(previewURL);
});

PNG 是这个例子的显式约束,不代表 ClipboardItem 在所有平台只能支持 PNG。原文“Chrome 目前只支持 PNG 图片”的表述属于 2021 年状态,应按目标浏览器版本核对可用类型。也不能因 MIME 字符串以 image/ 开头,就让任意数据进入富文本渲染路径。

写入图片与同一内容的多种表示

write() 接收 ClipboardItem 数组,而不是直接接收单个 item。每个 item 用 MIME 到 Blob、字符串或相应 Promise 的映射构造,具体可用形式和类型应按浏览器验证。原文图片例子先 fetch 图片,再调用 response.blob(),把 Blob 放进 ClipboardItem。跨域图片还取决于 CORS,HTTP 返回错误也应处理。

原文多格式示例有两个可以静态确认的问题:函数 copy() 没有 async 却使用 await;fetch() 返回的 Response 被直接作为 image/png 数据使用。下面是标明差异的整理版,图片内容取自用户自己选择的 PNG 文件,避免依赖远程示例图片和网络等待消耗用户激活。图像与文字在同一个 ClipboardItem 内,不同应用可选择适合的表示。

// imageInput 是 type="file" 的文件输入框,copyImageButton 是复制按钮。
copyImageButton.addEventListener('click', async () => {
  try {
    const file = imageInput.files?.[0];
    if (!file || file.type !== 'image/png') {
      status.textContent = '请先选择 PNG 文件';
      return;
    }
    if (!navigator.clipboard?.write || typeof ClipboardItem === 'undefined') {
      status.textContent = '当前环境不支持复制图片';
      return;
    }
    const item = new ClipboardItem({
      'text/plain': new Blob(['用户选择的 PNG 图片'], { type: 'text/plain' }),
      'image/png': file
    });
    await navigator.clipboard.write([item]);
    status.textContent = '已复制图片及文字说明';
  } catch {
    status.textContent = '复制图片失败,请使用保存文件等替代方式。';
  }
});

这里的 file.type 只是示例前置筛选,不是恶意文件安全检测;面向上传、持久化或富文本导入的产品仍应进行相应验证。也不要承诺一次 write 能把多个独立剪贴项完整写入所有操作系统:底层剪贴板及接收应用可能只支持部分表示。

copy、cut:定制用户已经发起的操作

用户发起复制时会触发 copy 事件。event.clipboardData 提供 DataTransfer 接口,setData(type, data) 设置表示,getData(type) 读取可访问的表示,clearData([type]) 清除当前可写数据,items 列出剪贴项。并非每种事件都允许任意读写;不要把这些方法理解为可脱离事件直接操控整个系统剪贴板。

原文把被选文字转成大写的例子可整理为:只对指定 .source 区域生效,写入 text/plain 后取消默认复制。取消默认行为意味着应用必须实际提供有用的数据,不能先取消再发现处理失败。

const source = document.querySelector('.source');
source.addEventListener('copy', (event) => {
  const selection = document.getSelection();
  if (!selection || !event.clipboardData) return;
  event.clipboardData.setData('text/plain', selection.toString().toUpperCase());
  event.preventDefault();
});

cut 事件有相似的数据访问方式,但编辑器还要负责被剪切内容的删除及光标位置;取消默认剪切后只写剪贴板,不会自动替应用完成所有编辑行为。原文另一个 copy 图片示例把 DataTransferItem 直接传给 ClipboardItem。两者并不是同一类对象,不能互换;文件项需要在允许访问的事件中通过 getAsFile() 得到 File/Blob,再按目标 API 处理。而 copy 事件也不保证会带来可直接读取的图片项。本文不把这段原代码作为可运行图片复制方案,使用前节明确的数据来源替代。

paste:优先使用这次事件已提供的数据

原文在 paste 监听器中先 preventDefault,再调用异步 readText。整理稿改为从 event.clipboardData.getData("text/plain") 获取这次用户粘贴提供的文本,避免无谓地再次请求异步剪贴板读取权限。下面针对 textarea 保留选区替换行为;确认存在纯文本类型后才取消默认处理。

output.addEventListener('paste', (event) => {
  const data = event.clipboardData;
  if (!data || !Array.from(data.types).includes('text/plain')) return;
  event.preventDefault();
  output.setRangeText(data.getData('text/plain'),
    output.selectionStart, output.selectionEnd, 'end');
  status.textContent = '已粘贴纯文本';
});

富文本编辑器若必须接受 HTML,需要独立的清理策略;不要因为数据由浏览器粘贴事件提供,就认为它可信。用户复制的内容仍可能包含危险链接、不需要的样式或恶意标记。事件 API 与异步 API 是两种不同的入口,选择时应看用户是在点击“读取”按钮,还是已经发起了系统粘贴操作。

来源、修订与许可

原作者:阮一峰,《剪贴板操作 Clipboard API 教程》,2021-01-20。原文版权声明为“自由转载-非商用-非衍生-保持署名”,链接至 CC BY-NC-ND 3.0。保留原作者署名,修订不代表原作者观点。

补充核对:MDN Clipboard API。原文参考链接仍保留:Unblocking clipboard access、Interact with the clipboard、Multi-MIME Type Copying with the Async Clipboard API。核验日期 2026-10-05,所有示例仅做静态审查,兼容性需在目标浏览器实际验证。

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享
评论 抢沙发

请登录后发表评论

    暂无评论内容