取消屏蔽剪贴板访问权限

更安全、更顺畅地访问剪贴板中的文字和图片

访问系统剪贴板的传统方法是通过 document.execCommand() 进行剪贴板互动。虽然这种剪切和粘贴方法得到广泛支持,但其代价是:剪贴板访问是同步的,并且只能对 DOM 执行读写操作。

这对于少量文本没有问题,但在很多情况下,阻止网页进行剪贴板传输会给用户带来糟糕的体验。可能需要执行耗时的清理或图片解码,才能安全地粘贴内容。浏览器可能需要从粘贴的文档加载或内嵌链接的资源。这会导致在等待磁盘或网络时阻塞页面。想象一下,向混合权限中添加权限,要求浏览器在请求剪贴板访问权限时屏蔽相应网页。同时,针对剪贴板交互的 document.execCommand() 设置的权限较为宽松,并且因浏览器而异。

Async Clipboard API 可以解决这些问题,它提供了一个定义完善且不会阻塞网页的权限模型。在大多数浏览器上,Async Clipboard API 仅限处理文本和图片,但具体支持情况各有不同。请务必仔细研究以下各个部分的浏览器兼容性概览。

复制:将数据写入剪贴板

writeText()

如需将文本复制到剪贴板,请调用 writeText()。由于此 API 是异步的,因此 writeText() 函数会返回一个解析或拒绝的 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);
  }
}

浏览器支持

  • 66
  • 79
  • 63
  • 13.1

来源

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);
}

浏览器支持

  • 66
  • 79
  • 13.1

来源

复制事件

如果用户启动了剪贴板复制操作并且没有调用 preventDefault()copy 事件包含一个 clipboardData 属性,并且该项已采用正确的格式。如果您要实现自己的逻辑,则需要调用 preventDefault(),以防止默认行为改为使用您自己的实现方式。在这种情况下,clipboardData 将为空。 假设有一个包含文本和图片的网页,当用户全选并启动剪贴板复制操作时,您的自定义解决方案应舍弃文本,仅复制图片。您可以参考以下代码示例来实现此目的。本示例未涵盖在 Clipboard 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 事件:

浏览器支持

  • 1
  • 12
  • 22
  • 3

来源

对于 ClipboardItem

浏览器支持

  • 76
  • 79
  • 13.1

来源

粘贴:从剪贴板读取数据

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);
  }
}

浏览器支持

  • 66
  • 79
  • 13.1

来源

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);
  }
}

浏览器支持

  • 66
  • 79
  • 13.1

来源

处理粘贴的文件

能够使用剪贴板键盘快捷键(例如 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());
});

浏览器支持

  • 3
  • 12
  • 3.6
  • 4

来源

粘贴事件

如前所述,我们计划引入支持 Clipboard API 的事件,但目前您可以使用现有的 paste 事件。它可以很好地与用于读取剪贴板文本的新异步方法结合使用。与 copy 事件一样,请务必调用 preventDefault()

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

浏览器支持

  • 1
  • 12
  • 22
  • 3

来源

处理多个 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 /解压缩炸弹图片复制到剪贴板。

请求用户授予剪贴板权限的浏览器提示。
Clipboard API 的权限提示。

让网页不受限制地读取剪贴板中的内容会更加棘手。用户经常将密码和个人详细信息等敏感信息复制到剪贴板,然后在用户不知情的情况下被任何页面读取。

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

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 选项控制是否需要使用用户手势来调用剪切或粘贴操作。此值的默认值因浏览器而异,因此您应始终将其包含在内。

这时 Clipboard API 异步性质便派上用场了:如果用户尝试读取或写入剪贴板数据,系统会在用户尚未授予权限时自动提示。由于 API 是基于 promise 的,因此这是完全透明的,如果用户拒绝剪贴板权限,会导致 promise 拒绝,以便页面可以做出适当的响应。

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

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

权限政策集成

如需在 iframe 中使用该 API,您需要使用权限政策启用该 API,其中定义了一种机制,允许选择性地启用和停用各种浏览器功能和 API。具体而言,您需要传递 clipboard-readclipboard-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。在 Glitch 上,您可以混剪文本演示图片演示来做实验。

第一个示例演示了如何将文本移入和移出剪贴板。

如需尝试对 API 使用图片,请使用此演示。回想一下,目前仅支持 PNG 格式,而且只有少数浏览器支持这种格式。

致谢

Aasync Clipboard API 由 Darwin HuangGary Kačmarčík 实现。Darwin 还提供了演示。 感谢 Kyarik 和 Gary Kačmarčík 审核本文部分内容。

主打图片,由 Markus Winkler 制作 (Unsplash)。