截图这个需求看起来很简单:点个按钮,生成一张图。但真做起来你会发现它分成两类完全不同的问题:
- 把某个 DOM 区域“渲染成图片”(类似 html2canvas 的思路)
- 直接捕获屏幕/窗口/标签页的一帧(更像“录屏里截一帧”)
这篇文章按这两条路线各给一个原生实现的 demo,顺带把坑点说清楚;最后再补两个常用开源库的用法,方便你直接落地。
使用 Canvas 绘制 DOM 元素
核心原理
先把话说重一点:“纯原生把 DOM 画到 canvas”本质是在复刻 html2canvas。浏览器没有提供“一键把 DOM 截成图”的官方 API,所以你只能自己遍历节点、算布局、画背景、画文字、画图片……这条路能走,但边角非常多。
这里讲一个最简版本的思路:解析 DOM 树,用 Canvas 2D API 把你关心的信息画出来,最后导出图片。
遍历 DOM 树:从目标元素开始递归遍历所有子元素。
获取计算样式:利用 window.getComputedStyle 获取每个元素的最终样式(背景、字体、边框等)。
定位绘制:根据元素的 getBoundingClientRect() 数据,计算相对于目标区域的位置。
使用 Canvas 绘制:调用 Canvas 2D API,将每个元素绘制到 canvas 上,再通过 canvas.toDataURL() 生成图片。
代码示例
由于真实世界的 CSS 特性太复杂(字体、行高、阴影、transform、渐变、图片、svg、伪元素……),下面这个示例刻意做了简化:只处理背景色和文本节点,用来帮助你理解“为什么自己写会很痛苦”。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96
| <!DOCTYPE html> <html lang="zh-CN"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1"> <title>原生截图示例</title> <style> #capture { width: 400px; padding: 20px; background: #f5f5f5; border: 1px solid #ccc; } #capture h1 { color: #333; font-size: 24px; margin: 0; } #capture p { color: #666; font-size: 16px; } </style> </head> <body> <div id="capture"> <h1>示例标题</h1> <p>这是一个简单的示例,用于演示如何原生实现截图。</p> </div> <button id="btnCapture">截取 #capture</button> <br> <img id="resultImg" alt="截图结果" />
<script> function captureElement(element) { const rect = element.getBoundingClientRect(); const canvas = document.createElement('canvas'); canvas.width = rect.width; canvas.height = rect.height; const ctx = canvas.getContext('2d');
function renderNode(node) { if (node.nodeType === Node.ELEMENT_NODE) { const style = window.getComputedStyle(node); const nodeRect = node.getBoundingClientRect(); const x = nodeRect.left - rect.left; const y = nodeRect.top - rect.top;
const bgColor = style.backgroundColor; if (bgColor && bgColor !== 'rgba(0, 0, 0, 0)' && bgColor !== 'transparent') { ctx.fillStyle = bgColor; ctx.fillRect(x, y, nodeRect.width, nodeRect.height); }
Array.from(node.childNodes).forEach(child => renderNode(child)); } else if (node.nodeType === Node.TEXT_NODE) { const text = node.textContent.trim(); if (text) { const parentStyle = window.getComputedStyle(node.parentNode); const font = parentStyle.font; const color = parentStyle.color; const parentRect = node.parentNode.getBoundingClientRect(); const x = parentRect.left - rect.left; const y = parentRect.top - rect.top + parseInt(parentStyle.fontSize, 10); ctx.fillStyle = color; ctx.font = font; ctx.fillText(text, x, y); } } }
renderNode(element); return canvas; }
document.getElementById('btnCapture').addEventListener('click', function () { const captureElementNode = document.getElementById('capture'); const canvas = captureElement(captureElementNode); const imgData = canvas.toDataURL('image/png'); document.getElementById('resultImg').src = imgData; }); </script> </body> </html>
|
代码解析:
创建 canvas:根据目标元素 #capture 的尺寸创建 canvas,确保捕获的图片与实际尺寸一致。
递归遍历 DOM:renderNode 函数递归遍历目标元素的所有子节点,对每个元素获取其 getBoundingClientRect() 数据,并调用 getComputedStyle 获取样式。
绘制背景与文本:
输出图片:最后,将 canvas 内容转换为 DataURL,并设置到 <img> 标签中展示。
核心原理
getDisplayMedia 走的是另一条路:它不是在“截图 DOM”,而是在“捕获屏幕媒体流”。用户选择共享一个屏幕/窗口/标签页之后,你拿到的是视频流(MediaStream),再从里面抓一帧画到 canvas 导出图片。
实例代码
下面的示例展示了如何使用 getDisplayMedia 捕获屏幕,并将一帧图像绘制到 canvas 上,然后生成图片并展示给用户。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60
| <!DOCTYPE html> <html lang="zh-CN"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>原生实现截图示例</title> <style> body { font-family: sans-serif; text-align: center; padding: 20px; } #resultImg { margin-top: 20px; max-width: 100%; border: 1px solid #ccc; } </style> </head> <body> <h1>原生截图示例</h1> <button id="captureBtn">点击截屏</button> <br> <img id="resultImg" alt="截图结果" />
<script> const captureBtn = document.getElementById('captureBtn'); const resultImg = document.getElementById('resultImg');
captureBtn.addEventListener('click', async () => { try { const stream = await navigator.mediaDevices.getDisplayMedia({ video: true }); const video = document.createElement('video'); video.srcObject = stream; await video.play();
video.onloadedmetadata = () => { const canvas = document.createElement('canvas'); canvas.width = video.videoWidth; canvas.height = video.videoHeight; const ctx = canvas.getContext('2d'); ctx.drawImage(video, 0, 0, canvas.width, canvas.height);
const imgData = canvas.toDataURL('image/png'); resultImg.src = imgData;
stream.getTracks().forEach(track => track.stop()); }; } catch (error) { console.error("截图失败:", error); } }); </script> </body> </html>
|
代码解析:
getDisplayMedia
当点击按钮时,通过 navigator.mediaDevices.getDisplayMedia({ video: true }) 请求用户授权屏幕捕获,返回一个视频流(MediaStream)。
video 元素播放流
创建一个 video 元素,并将视频流设置为其 srcObject,启动视频播放以确保能捕获到有效帧。
canvas 绘制与截图
在 video.onloadedmetadata 事件中,根据视频尺寸创建一个 canvas,然后调用 ctx.drawImage(video, 0, 0) 将当前视频帧绘制到 canvas 上。最后,调用 canvas.toDataURL('image/png') 将 canvas 内容转换为 Base64 格式的图片数据。
释放资源
捕获截图后,通过停止视频流中所有轨道来释放资源。
注意事项
用户授权
getDisplayMedia 必须用户手动授权,而且用户可以选整个屏幕、某个窗口或某个标签页。也就是说:你无法像“截 DOM”那样精确控制范围,它截的就是用户选中的那一块。
捕获内容范围
因为捕获的是媒体流,如果你只想截一个 DOM 区域,通常要把它单独放到一个页面/窗口里,让用户选择那个窗口/标签页,否则很难只拿到“页面的一小块”。
跨域资源问题
如果你走的是“画 DOM 到 canvas”那条路,跨域图片经常会把 canvas 标记成 tainted,导致 toDataURL() 直接报错;这类资源要么走 CORS,要么就只能接受“截不出来”的现实。
性能开销
屏幕捕获和 canvas 绘制均有一定性能开销,建议仅在用户明确需要截图时调用,并适当提供 loading 状态反馈。
使用开源工具包
说实话,生产环境里要做 DOM 截图,我基本不会建议自己从 0 手写遍历和绘制逻辑:成本太高,坑太多。更现实的选择是直接用成熟库。下面简单过一下 html2canvas 和 dom-to-image 这两个老牌选手。
HTML2Canvas
HTML2Canvas 是一个流行的 JavaScript 库,它通过遍历 DOM,将页面上的内容”拍照”并转换为 canvas 元素,进而生成图片数据(Base64 或 Blob 格式)。
优点:
缺点:
对复杂的 CSS 样式和跨域图片可能存在限制
截图精度可能受限于浏览器渲染
工具的安装使用代码示例
1 2 3 4
| npm install html2canvas --save
<script src="https://cdn.jsdelivr.net/npm/html2canvas@1.4.1/dist/html2canvas.min.js"></script>
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38
| <!DOCTYPE html> <html lang="zh-CN"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>网页截图示例</title> <style> #capture { padding: 20px; background: #f5f5f5; border: 1px solid #ccc; } </style> </head> <body> <div id="capture"> <h1>这是需要截图的区域</h1> <p>这里包含一些文本和样式。</p> </div> <button id="btnCapture">截图</button> <img id="resultImage" alt="截图结果" />
<script src="https://cdn.jsdelivr.net/npm/html2canvas@1.4.1/dist/html2canvas.min.js"></script> <script> document.getElementById('btnCapture').addEventListener('click', function () { const captureElement = document.getElementById('capture'); html2canvas(captureElement).then(canvas => { const imgData = canvas.toDataURL('image/png'); document.getElementById('resultImage').src = imgData; }).catch(err => { console.error('截图失败:', err); }); }); </script> </body> </html>
|
dom-to-image
dom-to-image 也是一个流行的库,它可以将 DOM 节点转换为图片(SVG、PNG、JPEG 等格式)。
优点:
支持多种图片格式
提供更多配置选项,可以定制输出效果
缺点:
- 对复杂样式和嵌入资源(例如跨域图片)可能需要额外配置
安装与使用代码示例
1 2 3 4
| npm install dom-to-image --save
<script src="https://cdn.jsdelivr.net/npm/dom-to-image@2.6.0/src/dom-to-image.min.js"></script>
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38
| <!DOCTYPE html> <html lang="zh-CN"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>dom-to-image 截图示例</title> <style> #capture { padding: 20px; background: #eef; border: 1px solid #99c; } </style> </head> <body> <div id="capture"> <h1>这是需要截图的区域</h1> <p>这里包含一些文本和样式。</p> </div> <button id="btnCapture">截图</button> <img id="resultImage" alt="截图结果" />
<script src="https://cdn.jsdelivr.net/npm/dom-to-image@2.6.0/src/dom-to-image.min.js"></script> <script> document.getElementById('btnCapture').addEventListener('click', function () { const node = document.getElementById('capture'); domtoimage.toPng(node) .then(function (dataUrl) { const img = document.getElementById('resultImage'); img.src = dataUrl; }) .catch(function (error) { console.error('截图失败!', error); }); }); </script> </body> </html>
|
如果你是为了业务里落地截图功能:DOM 截图优先考虑成熟库;要“截屏/截窗口/截标签页”,那就从 getDisplayMedia 这条路入手,心智模型会清晰很多。
Happy Coding!