Async Clipboard API 中未经过排错的 HTML

从 Chrome 120 开始,Async Clipboard API 中提供了新的 unsanitized 选项。此选项适用于使用 HTML 的特殊情况,即您需要粘贴剪贴板中的内容,且粘贴内容的方式与复制时相同。也就是说,浏览器无需执行任何通常(并且有充分的理由)的中间清理步骤。请参阅本指南,了解如何使用它。

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

对于文字来说,这是绝对正确的。请尝试将以下代码粘贴到开发者工具控制台中,然后立即重新聚焦此页面。(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 代码(以及 <meta><head><style> 等其他代码)中剥离 <script> 标记以及内嵌 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 的网页自定义格式。停止阅读此处,查看链接的文章。
  2. 如果您仅控制应用中的粘贴端,而不控制复制端,这可能是因为复制操作发生在不支持网页自定义格式的原生应用中,则应使用 unsanitized 选项(本文的其余部分对此进行了说明)。

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

复制并粘贴未清理的 HTML

当您使用 Async Clipboard API 将 HTML write()(复制)复制到剪贴板时,浏览器会通过 DOM 解析器运行并序列化生成的 HTML 字符串,以确保其格式正确,但此步骤不会进行任何清理。您无需执行任何操作。当您 read() 由其他应用放置在剪贴板中的 HTML 时,您的 Web 应用已选择获取全保真内容并需要在您自己的代码中执行任何清理,那么您可以向 read() 方法传递一个属性为 unsanitized 且值为 ['text/html'] 的选项对象。单独时,看起来像这样:navigator.clipboard.read({ unsanitized: ['text/html'] })。以下代码示例与之前显示的代码示例几乎相同,但这次使用的是 unsanitized 选项。当您在开发者工具控制台中试用此功能时,您会发现输入和输出是相同的。

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 团队指定和实现。