取消屏蔽剪贴板访问权限

更安全、不受限的剪贴板访问权限,可用于文字和图片

获取系统剪贴板的传统方式是通过 document.execCommand() 进行剪贴板交互。虽然这种剪切和粘贴方法得到了广泛支持,但却存在一个缺点:剪贴板访问是同步的,并且只能读取和写入 DOM。

对于少量文本来说,这没问题,但在许多情况下,阻止网页进行剪贴板转移会带来糟糕的体验。在安全粘贴内容之前,可能需要进行耗时的清理或图片解码。浏览器可能需要加载或内嵌粘贴文档中的关联资源。这样会在等待磁盘或网络时阻塞网页。假设我们添加了权限,要求浏览器在请求剪贴板访问权限时阻止网页。与此同时,围绕 document.execCommand() 放置的剪贴板交互权限定义松散,并且因浏览器而异。

异步剪贴板 API 可解决这些问题,提供明确定义的权限模型,不会阻塞网页。在大多数浏览器中,Async Clipboard API 仅限于处理文本和图片,但支持情况各不相同。请务必仔细研究以下各部分的浏览器兼容性概览。

复制:将数据写入剪贴板

writeText()

如需将文本复制到剪贴板,请调用 writeText()。由于此 API 是异步的,因此 writeText() 函数会返回一个 Promise,该 Promise 会根据传递的文本是否成功复制而解析或拒绝:

async function copyPageUrl() {
  try {
    await navigator.clipboard.writeText(location.href);
    console.log('Page URL copied to clipboard');
  } catch (err) {
    console.error('Failed to copy: ', err);
  }
}

Browser Support

  • Chrome: 66.
  • Edge: 79.
  • Firefox: 63.
  • Safari: 13.1.

Source

write()

实际上,writeText() 只是通用 write() 方法的便捷方法,该方法还可让您将图片复制到剪贴板。与 writeText() 一样,它是异步的,并返回一个 Promise。

如需将图片写入剪贴板,您需要将图片作为 blob。一种方法是使用 fetch() 从服务器请求图片,然后对响应调用 blob()

由于各种原因,从服务器请求图片可能并不理想或不可行。幸运的是,您还可以将图片绘制到画布上,然后调用画布的 toBlob() 方法。

接下来,将 ClipboardItem 对象数组作为参数传递给 write() 方法。目前,您一次只能传递一张图片,但我们希望未来能添加对多张图片的支持。ClipboardItem 接受一个对象,该对象以图片的 MIME 类型作为键,以 blob 作为值。对于从 fetch()canvas.toBlob() 获取的 blob 对象,blob.type 属性会自动包含正确的图片 MIME 类型。

try {
  const imgURL = '/images/generic/file.png';
  const data = await fetch(imgURL);
  const blob = await data.blob();
  await navigator.clipboard.write([
    new ClipboardItem({
      // The key is determined dynamically based on the blob's type.
      [blob.type]: blob
    })
  ]);
  console.log('Image copied.');
} catch (err) {
  console.error(err.name, err.message);
}

或者,您也可以向 ClipboardItem 对象写入 promise。 对于此模式,您需要预先知道数据的 MIME 类型。

try {
  const imgURL = '/images/generic/file.png';
  await navigator.clipboard.write([
    new ClipboardItem({
      // Set the key beforehand and write a promise as the value.
      'image/png': fetch(imgURL).then(response => response.blob()),
    })
  ]);
  console.log('Image copied.');
} catch (err) {
  console.error(err.name, err.message);
}

Browser Support

  • Chrome: 76.
  • Edge: 79.
  • Firefox: 127.
  • Safari: 13.1.

Source

复制活动

如果用户发起剪贴板复制操作,但调用 preventDefault(),则 copy 事件会包含 clipboardData 属性,其中包含的项已采用正确的格式。如果您想实现自己的逻辑,则需要调用 preventDefault() 以防止出现默认行为,转而执行您自己的实现。 在这种情况下,clipboardData 将为空。 假设某个网页包含文本和图片,当用户选择所有内容并开始复制到剪贴板时,您的自定义解决方案应舍弃文本,仅复制图片。您可以按以下代码示例所示的方式实现此目的。 此示例未涵盖在不支持剪贴板 API 时如何回退到更早的 API。

<!-- The image we want on the clipboard. -->
<img src="kitten.webp" alt="Cute kitten.">
<!-- Some text we're not interested in. -->
<p>Lorem ipsum</p>
document.addEventListener("copy", async (e) => {
  // Prevent the default behavior.
  e.preventDefault();
  try {
    // Prepare an array for the clipboard items.
    let clipboardItems = [];
    // Assume `blob` is the blob representation of `kitten.webp`.
    clipboardItems.push(
      new ClipboardItem({
        [blob.type]: blob,
      })
    );
    await navigator.clipboard.write(clipboardItems);
    console.log("Image copied, text ignored.");
  } catch (err) {
    console.error(err.name, err.message);
  }
});

对于 copy 事件:

Browser Support

  • Chrome: 1.
  • Edge: 12.
  • Firefox: 22.
  • Safari: 3.

Source

对于 ClipboardItem

Browser Support

  • Chrome: 76.
  • Edge: 79.
  • Firefox: 127.
  • Safari: 13.1.

Source

粘贴:从剪贴板读取数据

readText()

如需从剪贴板读取文本,请调用 navigator.clipboard.readText() 并等待返回的 promise 解析:

async function getClipboardContents() {
  try {
    const text = await navigator.clipboard.readText();
    console.log('Pasted content: ', text);
  } catch (err) {
    console.error('Failed to read clipboard contents: ', err);
  }
}

Browser Support

  • Chrome: 66.
  • Edge: 79.
  • Firefox: 125.
  • Safari: 13.1.

Source

read()

navigator.clipboard.read() 方法也是异步的,并返回一个 promise。如需从剪贴板读取图片,请获取 ClipboardItem 对象的列表,然后遍历这些对象。

每个 ClipboardItem 可以采用不同的类型来保存其内容,因此您需要再次使用 for...of 循环来遍历类型列表。对于每种类型,请调用 getType() 方法,并将当前类型作为实参传递,以获取相应的 blob。与之前一样,此代码不与图片相关联,并且适用于未来的其他文件类型。

async function getClipboardContents() {
  try {
    const clipboardItems = await navigator.clipboard.read();
    for (const clipboardItem of clipboardItems) {
      for (const type of clipboardItem.types) {
        const blob = await clipboardItem.getType(type);
        console.log(URL.createObjectURL(blob));
      }
    }
  } catch (err) {
    console.error(err.name, err.message);
  }
}

Browser Support

  • Chrome: 76.
  • Edge: 79.
  • Firefox: 127.
  • Safari: 13.1.

Source

处理粘贴的文件

用户能够使用剪贴板键盘快捷键(例如 Ctrl+cCtrl+v)非常有用。 Chromium 会按如下所述在剪贴板上公开只读文件。 当用户按下操作系统的默认粘贴快捷键或在浏览器的菜单栏中依次点击修改粘贴时,系统会触发此事件。 无需进一步编写管道代码。

document.addEventListener("paste", async e => {
  e.preventDefault();
  if (!e.clipboardData.files.length) {
    return;
  }
  const file = e.clipboardData.files[0];
  // Read the file's contents, assuming it's a text file.
  // There is no way to write back to it.
  console.log(await file.text());
});

Browser Support

  • Chrome: 3.
  • Edge: 12.
  • Firefox: 3.6.
  • Safari: 4.

Source

粘贴事件

如前所述,我们计划引入与剪贴板 API 搭配使用的事件,但目前您可以使用现有的 paste 事件。它与用于读取剪贴板文本的新异步方法配合得很好。与 copy 事件一样,别忘了调用 preventDefault()

document.addEventListener('paste', async (e) => {
  e.preventDefault();
  const text = await navigator.clipboard.readText();
  console.log('Pasted text: ', text);
});

Browser Support

  • Chrome: 1.
  • Edge: 12.
  • Firefox: 22.
  • Safari: 3.

Source

处理多种 MIME 类型

大多数实现都会在剪贴板上放置多种数据格式,以用于单个剪切或复制操作。这有两个原因:作为应用开发者,您无法知道用户想要将文本或图片复制到的应用的功能;许多应用支持将结构化数据粘贴为纯文本。此功能通常通过名称为粘贴并匹配样式粘贴时不保留格式修改菜单项提供给用户。

以下示例展示了如何执行此操作。此示例使用 fetch() 获取图片数据,但图片数据也可以来自 <canvas>File System Access API

async function copy() {
  const image = await fetch('kitten.png').then(response => response.blob());
  const text = new Blob(['Cute sleeping kitten'], {type: 'text/plain'});
  const item = new ClipboardItem({
    'text/plain': text,
    'image/png': image
  });
  await navigator.clipboard.write([item]);
}

安全与权限

剪贴板访问权限一直是浏览器面临的安全问题。如果没有适当的权限,网页可能会在用户不知情的情况下将各种恶意内容复制到用户的剪贴板,并在粘贴时产生灾难性后果。假设某个网页在您不知情的情况下将 rm -rf /解压缩炸弹图片复制到您的剪贴板。

浏览器提示,询问用户是否授予剪贴板权限。
剪贴板 API 的权限提示。

让网页不受限制地读取剪贴板内容会带来更多麻烦。用户经常将密码和个人详细信息等敏感信息复制到剪贴板,而任何网页都可以在用户不知情的情况下读取这些信息。

与许多新 API 一样,剪贴板 API 仅支持通过 HTTPS 提供的网页。为帮助防止滥用,只有当网页是活跃标签页时,才允许访问剪贴板。处于活动状态的标签页中的网页无需请求权限即可写入剪贴板,但读取剪贴板内容始终需要权限。

已向 Permissions API 添加了复制和粘贴权限。 当网页是活动标签页时,系统会自动向其授予 clipboard-write 权限。必须请求 clipboard-read 权限,您可以通过尝试从剪贴板读取数据来请求该权限。以下代码展示了后者:

const queryOpts = { name: 'clipboard-read', allowWithoutGesture: false };
const permissionStatus = await navigator.permissions.query(queryOpts);
// Will be 'granted', 'denied' or 'prompt':
console.log(permissionStatus.state);

// Listen for changes to the permission state
permissionStatus.onchange = () => {
  console.log(permissionStatus.state);
};

您还可以使用 allowWithoutGesture 选项控制是否需要用户手势才能调用剪切或粘贴操作。此值的默认值因浏览器而异,因此您应始终包含此值。

此时,剪贴板 API 的异步特性就派上了用场:尝试读取或写入剪贴板数据会自动提示用户授予权限(如果尚未授予)。由于该 API 基于 Promise,因此这是完全透明的,并且用户拒绝剪贴板权限会导致 Promise 拒绝,以便网页做出适当的响应。

由于浏览器仅在网页是活动标签页时允许访问剪贴板,因此您会发现,如果将此处的一些示例直接粘贴到浏览器的控制台中,它们不会运行,因为开发者工具本身就是活动标签页。有一个技巧:使用 setTimeout() 延迟剪贴板访问,然后在调用函数之前快速点击页面内部以聚焦页面:

setTimeout(async () => {
  const text = await navigator.clipboard.readText();
  console.log(text);
}, 2000);

权限政策集成

如需在 iframe 中使用该 API,您需要通过权限政策启用该 API,该政策定义了一种机制,可用于选择性地启用和停用各种浏览器功能和 API。具体而言,您需要传递 clipboard-read 和/或 clipboard-write,具体取决于应用的需求。

<iframe
    src="index.html"
    allow="clipboard-read; clipboard-write"
>
</iframe>

功能检测

如需使用 Async Clipboard API 并支持所有浏览器,请测试 navigator.clipboard 并回退到更早的方法。例如,以下展示了如何实现粘贴以包含其他浏览器。

document.addEventListener('paste', async (e) => {
  e.preventDefault();
  let text;
  if (navigator.clipboard) {
    text = await navigator.clipboard.readText();
  }
  else {
    text = e.clipboardData.getData('text/plain');
  }
  console.log('Got pasted text: ', text);
});

但事实并非如此。在 Async Clipboard API 出现之前,各网络浏览器中存在各种不同的复制和粘贴实现。在大多数浏览器中,可以使用 document.execCommand('copy')document.execCommand('paste') 触发浏览器自身的复制和粘贴功能。如果要复制的文本是 DOM 中不存在的字符串,则必须将其注入到 DOM 中并选中:

button.addEventListener('click', (e) => {
  const input = document.createElement('input');
  input.style.display = 'none';
  document.body.appendChild(input);
  input.value = text;
  input.focus();
  input.select();
  const result = document.execCommand('copy');
  if (result === 'unsuccessful') {
    console.error('Failed to copy text.');
  }
  input.remove();
});

演示

您可以在下面的演示中试用 Async Clipboard API。 第一个示例演示了如何将文本移入和移出剪贴板。

如需试用此 API,请使用此演示。请注意,仅支持 PNG,且仅在少数浏览器中支持。

致谢

异步剪贴板 API 由 Darwin HuangGary Kačmarčík 实现。Darwin 还提供了演示。 感谢 Kyarik 和 Gary Kačmarčík 再次审核本文的部分内容。

主打图片由 Markus Winkler 拍摄,选自 Unsplash