浅谈浏览器的剪切板 API 及其应用
前言
同事在使用公司中台系统时,出于反馈或咨询的需要,会对网页的中某一特定区域截图分享给他人。作为该中台系统的前端开发者,我在思考能否开发这个新功能:用户一键点击便可以来对特定区域进行精确的截图,沟通问题时也更加高效。
该功能的演示效果如下所示:
接下来,让我们一起看看是如何实现该功能的吧。
浏览器中的剪切板:Clipboard API
Clipboard
接口实现了 Clipboard API,如果用户授予了相应的权限,其就能提供系统剪贴板的读写访问能力。在 Web 应用程序中,Clipboard API 可用于实现剪切、复制和粘贴功能。Clipboard
是浏览器新提供的剪贴板操作方法,用于取代安全隐患比较大的 document.execCommand
。
Clipboard 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 标签,即生成好了图片。
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 数据:
canvas 绘制
剩下的工作就交给 canvas 绘制,再转成 Blob 二进制,最后写入剪贴板 Clipboard。
const img = new Image();
img.src = dataUrl;
canvas.getContext('2d').drawImage(img, 0, 0);
canvas.toBlob(writeBlobToClipboard);
总结
本文通过分析如何将 DOM 复制到剪贴板的过程,介绍了 Clipboard API 的特性以及 DOM 生成图片的思路,并帮助大家熟悉克隆 DOM 元素和样式的方法。由于生成 DOM 图片的操作也可以应用在其它业务场景,例如:弹窗图片、制作海报、制作名片、二维码截屏等,希望本文对大家有所帮助。
转载自:https://juejin.cn/post/7158370002674909192