Async Clipboard API 中未经过排错的 HTML

从 Chrome 120 开始,Async Clipboard API 中提供了新的 unsanitized 选项。在 HTML 的特殊情况下,此选项会很有用,因为您需要将剪贴板中的内容粘贴为与复制时相同的形式。也就是说,浏览器没有通常出于合理原因应用的任何中间清理步骤。如需了解如何使用此 API,请参阅本指南。

在使用 Async Clipboard API 时,在大多数情况下,开发者无需担心剪贴板上内容的完整性,并且可以假定他们写入剪贴板的内容(复制)与从剪贴板读取数据(粘贴)时得到的内容相同

这对于文本来说绝对正确。请尝试在 DevTools 控制台中粘贴以下代码,然后立即重新聚焦页面。(必须使用 setTimeout(),以便您有足够的时间将焦点移至页面,这是 Async Clipboard API 的要求。)如您所见,输入与输出完全相同。

setTimeout(async () => {
  const input = 'Hello';
  await navigator.clipboard.writeText(input);
  const output = await navigator.clipboard.readText();
  console.log(input, output, input === output);
  // Logs "Hello Hello true".
}, 3000);

图片的情况略有不同。为了防范所谓的压缩炸弹攻击,浏览器会重新编码 PNG 等图片,但输入图片和输出图片在视觉上完全相同,像素与像素完全一样。

setTimeout(async () => {
  const dataURL =
    'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVQYV2NgYAAAAAMAAWgmWQ0AAAAASUVORK5CYII=';
  const input = await fetch(dataURL).then((response) => response.blob());
  await navigator.clipboard.write([
    new ClipboardItem({
      [input.type]: input,
    }),
  ]);
  const [clipboardItem] = await navigator.clipboard.read();
  const output = await clipboardItem.getType(input.type);
  console.log(input.size, output.size, input.type === output.type);
  // Logs "68 161 true".
}, 3000);

那么,HTML 文本会怎样呢?您可能已经猜到了,HTML 的情况有所不同。在这里,浏览器会对 HTML 代码进行排错,以防止出现不良情况,例如从 HTML 代码中剥离 <script> 标记(以及 <meta><head><style> 等其他标记),并内嵌 CSS。请参考以下示例,然后在开发者工具控制台中尝试一下。您会发现,输出与输入有很大不同。

setTimeout(async () => {
  const input = `<html>  
  <head>  
    <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />  
    <meta name="ProgId" content="Excel.Sheet" />  
    <meta name="Generator" content="Microsoft Excel 15" />  
    <style>  
      body {  
        font-family: HK Grotesk;  
        background-color: var(--color-bg);  
      }  
    </style>  
  </head>  
  <body>  
    <div>hello</div>  
  </body>  
</html>`;
  const inputBlob = new Blob([input], { type: 'text/html' });
  await navigator.clipboard.write([
    new ClipboardItem({
      'text/html': inputBlob,
    }),
  ]);
  const [clipboardItem] = await navigator.clipboard.read();
  const outputBlob = await clipboardItem.getType('text/html');
  const output = await outputBlob.text();
  console.log(input, output);
}, 3000);

通常,HTML 清理是件好事。在大多数情况下,您不希望允许未经净化的 HTML,以免导致安全问题。不过,在某些情况下,开发者确切地知道自己在做什么,以及入站和输出 HTML 的完整性对于应用的正常运行至关重要。在这些情况下,您有两个选择:

  1. 如果您同时控制复制和粘贴端(例如,从应用内复制,然后同样在应用内粘贴),则应使用适用于 Async Clipboard API 的 Web 自定义格式。请停止阅读此处内容,并参阅链接的文章。
  2. 如果您只能控制应用中的粘贴端,而不能控制复制端(可能是因为复制操作发生在不支持 Web 自定义格式的原生应用中),则应使用 unsanitized 选项,本文的其余部分将对此进行介绍。

清理包括移除 script 标记、内嵌样式以及确保 HTML 格式正确等操作。此列表并非详尽无遗,未来可能会添加更多步骤。

复制并粘贴未经排错的 HTML

当您使用 Async Clipboard API 将 HTML write()(复制)到剪贴板时,浏览器会通过 DOM 解析器运行 HTML 并序列化生成的 HTML 字符串,以确保其格式正确无误,但此步骤不会进行任何净化操作。您无需执行任何操作。当您 read() 其他应用放置在剪贴板上的 HTML 时,如果您的 Web 应用选择获取完整保真度内容,并且需要在自己的代码中执行任何净化操作,您可以将一个选项对象传递给 read() 方法,其中包含属性 unsanitized 和值 ['text/html']。具体如下所示:navigator.clipboard.read({ unsanitized: ['text/html'] })。以下代码示例与之前显示的示例几乎相同,只是这次使用 unsanitized 选项。在 DevTools 控制台中试用时,您会发现输入和输出相同。

setTimeout(async () => {
  const input = `<html>  
  <head>  
    <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />  
    <meta name="ProgId" content="Excel.Sheet" />  
    <meta name="Generator" content="Microsoft Excel 15" />  
    <style>  
      body {  
        font-family: HK Grotesk;  
        background-color: var(--color-bg);  
      }  
    </style>  
  </head>  
  <body>  
    <div>hello</div>  
  </body>  
</html>`;
  const inputBlob = new Blob([input], { type: 'text/html' });
  await navigator.clipboard.write([
    new ClipboardItem({
      'text/html': inputBlob,
    }),
  ]);
  const [clipboardItem] = await navigator.clipboard.read({
    unsanitized: ['text/html'],
  });
  const outputBlob = await clipboardItem.getType('text/html');
  const output = await outputBlob.text();
  console.log(input, output);
}, 3000);

浏览器支持和功能检测

没有直接的方法来检查是否支持该功能,因此功能检测是基于观察行为进行的。因此,以下示例依赖于检测 <style> 标记是否保留(表示支持)或是否被内嵌(表示不支持)这一事实。请注意,为此,网页需要已获得剪贴板权限。

const supportsUnsanitized = async () => {
  const input = `<style>p{color:red}</style><p>a`;
  const inputBlob = new Blob([input], { type: 'text/html' });
  await navigator.clipboard.write([
    new ClipboardItem({
      'text/html': inputBlob,
    }),
  ]);
  const [clipboardItem] = await navigator.clipboard.read({
    unsanitized: ['text/html],
  });
  const outputBlob = await clipboardItem.getType('text/html');
  const output = await outputBlob.text();
  return /<style>/.test(output);
};

演示

如需查看 unsanitized 选项的实际运作方式,请参阅 Glitch 上的演示并查看其源代码

总结

如简介中所述,大多数开发者无需担心剪贴板的清理工作,只需使用浏览器提供的默认清理选项即可。对于开发者需要注意的极少数情况,unsanitized 选项可用。

致谢

本文由 Anupam SnigdhaRachel Andrew 审核。该 API 由 Microsoft Edge 团队指定和实现。