05-技术讲解
# 网页复制成图片至剪贴板技术讲解
# 什么问题
在网页中,经常能看到一键复制网页内容到剪贴板的功能。要实现这个功能倒并不困难,浏览器有 Clipboard API 来做和剪贴板相关的操作。
但是在一些 Web 应用中,还支持直接将网页复制成图片到剪贴板,例如飞书:
支持将网页某一部分内容复制成图片,然后放入剪贴板,这个就需要我们做一些额外的处理了。
# 解决思路
- 第一步是获取该区域的 DOM 元素
const ele = document.getElementById("target");
获取到该区域最外层的 DOM 元素后,需要做一些额外的处理:
- 将 DOM 转为 Canvas
- 将 Canvas 转为 Blob
- 将 Blob 写入到剪贴板
# 解决细节
1. 将 DOM 转为 Canvas
这会涉及到对整棵 DOM 树的解析,以及样式计算等工作。
整个流程整理下来:
- 初始化与配置:
- 接收用户传入的 HTML 元素及配置选项。
- 设置默认配置选项,并将用户配置与默认配置合并。
- 解析和计算样式:
- 遍历 DOM 树,收集所有需要绘制的元素。
- 使用 window.getComputedStyle 方法获取每个元素的计算样式。
- 处理 CSS 样式,包括背景、边框、阴影、字体、文本对齐等。
- 处理特殊元素:
- 图像:加载图像资源,处理跨域图像问题(CORS)。
- SVG:解析和绘制 SVG 元素。
- 伪元素:处理 ::before 和 ::after 伪元素,将它们视为普通 DOM 元素进行绘制。
- 创建 Canvas 元素:
- 创建一个新的 <canvas> 元素,获取其 2D 绘图上下文(context)。
- 根据目标元素的尺寸调整 Canvas 的宽度和高度。
- 绘制背景和边框:
- 根据元素的背景颜色、背景图像、背景渐变等属性,绘制背景。
- 绘制元素的边框,包括边框样式、宽度和颜色。
- 绘制文本:
- 处理字体样式(如字体家族、字号、字体颜色、行高等)。
- 使用 fillText 方法在 Canvas 上绘制文本内容。
- 绘制内容:
- 绘制图像内容,包括处理图像的缩放和定位。
- 处理其他内容元素,如内联 SVG 和 Canvas 元素本身。
- 处理叠加效果:
- 处理元素的阴影效果(box-shadow 和 text-shadow)。
- 处理混合模式和透明度。
- 递归绘制子元素:
- 递归遍历和绘制元素的子节点,确保所有子元素都按照正确的层级关系绘制。
- 使用深度优先搜索(DFS)方法确保绘制顺序正确。
- 绘制伪元素:处理和绘制元素的伪元素(::before 和 ::after),确保它们与普通元素一样正确绘制。
- 处理滚动和视口:
- 处理滚动视图,确保只绘制当前可见区域。
- 处理固定定位和绝对定位的元素。
- 处理剪切和蒙版:
- 处理元素的剪切路径(clip-path)
- 处理蒙版效果(mask)
- 最终生成 Canvas:将所有解析和绘制操作完成后,生成最终的 Canvas 图像。
下面是一个简单的概念性的示意代码:
function convertToCanvas(element, options) {
// 1. 初始化与配置
const config = mergeDefaultOptions(options);
// 2. 解析和计算样式
const nodeParser = new NodeParser(element, config);
const parsedNodes = nodeParser.parse();
// 3. 创建 Canvas 元素
const canvas = document.createElement('canvas');
const context = canvas.getContext('2d');
// 4. 递归绘制元素
parsedNodes.forEach(node => {
drawNode(context, node, config);
});
// 5. 返回生成的 Canvas
return canvas;
}
function drawNode(context, node, config) {
// 绘制背景
drawBackground(context, node, config);
// 绘制边框
drawBorders(context, node, config);
// 绘制内容
drawContent(context, node, config);
// 递归绘制子元素
node.children.forEach(child => {
drawNode(context, child, config);
});
}
function drawBackground(context, node, config) {
// 绘制背景颜色、图像等
}
function drawBorders(context, node, config) {
// 绘制边框
}
function drawContent(context, node, config) {
// 绘制文本、图像等内容
}
// ....
不过好在前端已经有了这样的库:html2canvas
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>HTML to Canvas</title>
</head>
<body>
<div id="content">
<h1>Hello, World!</h1>
<p>This is a simple example of converting HTML to Canvas.</p>
</div>
<button id="convert">Convert to Canvas</button>
<div id="canvas-container"></div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/html2canvas/0.5.0-beta4/html2canvas.min.js"></script>
<script>
document.getElementById("convert").addEventListener("click", function () {
const content = document.getElementById("content");
html2canvas(content)
.then((canvas) => {
// 将生成的 Canvas 添加到页面
document.getElementById("canvas-container").appendChild(canvas);
})
.catch((error) => {
console.error("Error converting HTML to Canvas:", error);
});
});
</script>
</body>
</html>
2. 将 Canvas 转为 Blob
Clipboard API 提供的 ClipboardItem 接口,并不支持直接将 base64 字符串写入,而是需要转换为 Blob 对象,之后再进一步封装为 ClipboardItem 后通过 navigator.clipboard.write 方法将其写入剪贴板。
示例代码如下:
async function copyBase64ToClipboard(base64Data) {
const blob = new Blob([base64Data], { type: 'image/png' }); // 这里假设 base64Data 是一个 PNG 图片的 base64 字符串
const clipboardItem = new ClipboardItem({ 'image/png': blob });
await navigator.clipboard.write([clipboardItem]);
}
注意:Blob 并不是流数据,但是它可以和流数据相互转换。Blob 对象表示一段不可变的原始数据,而流数据表示一个可以按需逐步读取或写入的数据序列。
Blob 的特性:
- Blob 是不可变的:一旦创建,Blob 的内容就不能被改变。
- Blob 可以表示二进制数据:例如文件数据、图像数据等。
- Blob 可以通过 slice 方法进行分割,生成新的 Blob 对象。
与流的转换:
- Blob 可以与流数据进行转换。例如,可以将 Blob 转换为 ReadableStream,以便逐步读取数据。
- 通过 Response 对象的 body 属性,可以将 Blob 包装为 ReadableStream:
const blob = new Blob(["Hello, world!"], { type: "text/plain" });
// 将 Blob 转换为流
const stream = blob.stream();
// 读取流数据
const reader = stream.getReader();
reader.read().then(({ done, value }) => {
if (done) {
console.log("Stream reading complete");
} else {
console.log("Stream data:", new TextDecoder().decode(value));
}
});
回到我们的代码里面:
function copyDivToImage() {
const el = document.getElementById("target"); // 拿到 DOM 元素
html2canvas(el).then((canvas) => {
// 转换为 canvas
// 调用 toBlob 转为 Blob 数据
canvas.toBlob(
(blob) => {
// 复制文件到剪贴板
},
"image/jpeg", // 文件的格式
1 // 图像压缩质量 0-1
);
});
}
得到 Blob 数据之后,就是将 Blob 数据放入到剪贴板里面:
function copyDivToImage() {
const el = document.getElementById("target");
html2canvas(el).then((canvas) => {
canvas.toBlob(
async (blob) => {
// 复制文件到剪贴板
try {
await navigator.clipboard.write([
new ClipboardItem({
[blob.type]: blob,
}),
]);
console.log("图像已成功复制到剪贴板");
} catch (err) {
console.error("无法复制图像到剪贴板", err);
}
},
"image/jpeg", // 文件的格式
1 // 图像压缩质量 0-1
);
});
}
3. 常见问题
- HTTP 环境下 clipboard 无法正常工作
navigator.clipboard 需要在 HTTPS 环境下才能正常工作。在 HTTP 环境下,Clipboard API 将无法使用。
文档地址:https://developer.mozilla.org/en-US/docs/Web/API/Clipboard_API
- 剪贴板对 JPEG 格式的图片支持比较差
浏览器在 Clipboard API 中通常支持的 MIME 类型包括 text/plain、text/html 和 image/png。这些是规范中明确规定必须支持的类型,而其他类型如 image/jpeg 可能并不一致地受到所有浏览器的支持。
因此我们还需要对上面的代码做参数调整:
function copyDivToImage() {
const el = document.getElementById("target");
html2canvas(el).then((canvas) => {
canvas.toBlob(
async (blob) => {
// 复制文件到剪贴板
try {
await navigator.clipboard.write([
new ClipboardItem({
[blob.type]: blob,
}),
]);
console.log("图像已成功复制到剪贴板");
} catch (err) {
console.error("无法复制图像到剪贴板", err);
}
},
"image/png", // 文件的格式
1 // 图像压缩质量 0-1
);
});
}
-EOF-