likes
comments
collection
share

浅谈浏览器的剪切板 API 及其应用

作者站长头像
站长
· 阅读数 38

前言

同事在使用公司中台系统时,出于反馈或咨询的需要,会对网页的中某一特定区域截图分享给他人。作为该中台系统的前端开发者,我在思考能否开发这个新功能:用户一键点击便可以来对特定区域进行精确的截图,沟通问题时也更加高效。

该功能的演示效果如下所示:

浅谈浏览器的剪切板 API 及其应用

接下来,让我们一起看看是如何实现该功能的吧。

浏览器中的剪切板:Clipboard API

Clipboard 接口实现了 Clipboard API,如果用户授予了相应的权限,其就能提供系统剪贴板的读写访问能力。在 Web 应用程序中,Clipboard API 可用于实现剪切、复制和粘贴功能。Clipboard 是浏览器新提供的剪贴板操作方法,用于取代安全隐患比较大的 document.execCommand

Clipboard API 目前属于比较新的 API,浏览器仍在逐步实现它,兼容性如下:

浅谈浏览器的剪切板 API 及其应用

需要注意:

  • navigator.clipboard 需要在安全域 https 下使用
  • 读写操作需要用户授权,权限已添加到 Permissions API 中
  • 页面处于活动选项卡才能调用

Clipboard 的所有操作都是异步的,返回 Promise 对象,支持将任意内容放入剪贴板,这就给将图片放入剪贴板提供能实现的可能。Clipboard 提供了两个写入剪贴板的方法,分别是:

writeText

navigator.clipboard.writeText 方法可以写入字符串到操作系统的剪切板,参数为要写入的 DOMString

await navigator.clipboard.writeText('hello, world!');

write

navigator.clipboard.write 方法支持写入任意数据(文本数据、二进制数据)到剪贴板。实践中,我们往往构造一个 ClipboardItem 作为参数传入。ClipboardItem 的构造函数接受一个对象作为参数,该对象的 key 是数据的 MIME 类型,value 是 Blob 类型对象数据。

下面例子演示如下复制图片到剪贴板。

const blob = await fetch('image-remote-url').then((r) => r.blob());

await navigator.clipboard.write([
  new ClipboardItem({
    [blob.type]: blob,
  }),
]);

现在我们已经成功向剪切板中写入图片了。如果再把网页中的 DOM 元素先转换成图片,再写入岂不是实现了一键复制功能!

将 DOM 转换成图片

社区里比较流行的 npm 库是 html2canvas 和 dom-to-image,他们都是将页面上显示的 DOM 元素,经过解析画在 canvas 上,最后在转换为 Image 或 SVG 格式。如果利用了 SVG 的 foreignObject 元素,逻辑会更加简单:在 foreignObject 中嵌入要渲染 HTML 标签,即生成好了图片。

浅谈浏览器的剪切板 API 及其应用

DOM 节点处理

首先需要对节点进行克隆。在克隆时由于需要对每个节点进行「特殊处理」(样式和资源等),因此不能直接去调用 Node.cloneNode 方法,而是手动遍历克隆。

克隆节点

如果 node 为 HTMLCanvasElement,则转成 DataURL 并返回 Image 对象。对于其他的节点,调用 cloneNode(false) 即可。

function copyNode(original) {
  if (original instanceof HTMLCanvasElement) {
    return new Promise((resolve, reject) => {
      const image = new Image();
      image.src = original.toDataURL();
      image.onload = resolve(image);
      image.onerror = reject;
    });
  }

  return original.cloneNode(false);
}

克隆子节点

还要克隆所有子节点 childNodes。

/**
 * @param {*} original 原始节点
 * @param {*} clone copyNode()返回的克隆节点
 * @returns 已克隆子节点的克隆节点
 */
function copyChildren(original, clone) {
  const children = original.childNodes;
  if (children.length === 0) return Promise.resolve(clone);

  return cloneChildrenInOrder(clone, Array.from(children)).then(clone);
}

把上面两步结合起来便可以成功克隆一个节点。

样式

DOM 结构有了,但是原有的样式却丢失了,因此需要对样式做额外的处理。

克隆样式

转换的目标是包含指定 DOM 的 SVG,那么一些 CSS 规则将不生效,就需要利用 getComputedStyle 获取原节点的样式,将样式赋给克隆节点,其中 CSS 属性优先级则通过 getPropertyPriority 获取,将样式转成内联样式来解决此问题。

function cloneStyle(original, clone) {
  const source = window.getComputedStyle(original);

  Array.from(source).forEach((name) => {
    clone.style.setProperty(
      name,
      source.getPropertyValue(name),
      source.getPropertyPriority(name),
    );
  });
}

处理伪元素

对于伪元素 :before:after 等伪元素无法克隆,所以需要提取出样式,作为 style 添加到节点当中。getComputedStyle 第二个参数支持指定一个伪元素字符串,用以查询到该伪元素的样式信息。

function clonePseudoElements(original, clone) {
  [':before', ':after'].forEach((pseudo) => {
    // 查询指定伪元素
    const style = window.getComputedStyle(original, pseudo);
    const content = style.getPropertyValue('content');
    generateCssRules(style, pseudo);
  });
}

图片链接

img 标签链接

对于 img 标签,需要将图片链接 src 转成 dataURL 形式。

<img src="https://static.huolala.cn/image/6ce5...262f8.svg">

需要转成:

<img src="data:image/svg+xml;base64,PD94b...XXXXX">

通过图片链接获取资源并处理成 DataURL 返回:

const encoder = new FileReader();
// 将blob类型转成DataURL
encoder.readAsDataURL(blob);

background 样式链接

类似地,对于样式使用了 background: url() 来设置背景图片的节点,也需要处理。

background: url('https://webapi.XXX.com/assets/default.png') 0% 0% / cover
  repeat scroll padding-box border-box rgba(0, 0, 0, 0);

需要转成 DataURL:

background: url('data:image/png;base64,iVBOR...CYII=') 0% 0% / cover repeat
  scroll padding-box border-box rgba(0, 0, 0, 0);

水印

业务系统生成的 DOM 图片可能会涉及到不同安全等级的数据,基于信息安全考虑,图片有必要添加水印。业务系统通常都会带上水印,但水印 DOM 往往都在外层结构中,如果克隆节点无水印则需要手动加上。使用 canvas 绘制一张带水印的背景图即可。

function createWaterMark(node, config) {
  return new Promise((resolve) => {
    const canvas = document.createElement('canvas');
    const ctx = canvas.getContext('2d');

    ctx.fillText(content, 50, 50);

    // canvas生成水印图片dataURL
    const backgroundUrl = canvas.toDataURL();

    // ...
  });
}

序列化与绘制

克隆节点序列化为 XML

到目前为止,已经拿到了结构被深度复制且样式内联化处理的克隆节点。目标是使用 foreignObject 元素嵌入目标节点并使用 SVG 渲染出来,使用 XMLSerializer.serializeToString 将节点序列化成一串 XML 字符串。

const xml = new XMLSerializer().serializeToString(node);
const svg = `<svg ...><foreignObject x="0" y="0" width="100%" height="100%">${xml}</foreignObject></svg>`;

const dataUrl = `data:image/svg+xml;charset=utf-8,${svg}`;

至此,我们已经拿到已嵌入节点 SVG 的 DataURL 数据:

浅谈浏览器的剪切板 API 及其应用

canvas 绘制

剩下的工作就交给 canvas 绘制,再转成 Blob 二进制,最后写入剪贴板 Clipboard。

const img = new Image();
img.src = dataUrl;
canvas.getContext('2d').drawImage(img, 0, 0);

canvas.toBlob(writeBlobToClipboard);

完整 Demo 代码

总结

本文通过分析如何将 DOM 复制到剪贴板的过程,介绍了 Clipboard API 的特性以及 DOM 生成图片的思路,并帮助大家熟悉克隆 DOM 元素和样式的方法。由于生成 DOM 图片的操作也可以应用在其它业务场景,例如:弹窗图片、制作海报、制作名片、二维码截屏等,希望本文对大家有所帮助。

转载自:https://juejin.cn/post/7158370002674909192
评论
请登录