简介:这是一份面向前端开发者的原生 JavaScript 与 Canvas 截图下载示例资源,对应「原生js+canvas实现截图,实现a标签下载」主题,适合想在不引入第三方库的前提下理解网页截图与浏览器下载链路的初中级开发者,也适合作为课程演示与个人练手素材。压缩包为 rar 格式,仅含 1 个 html 文件,体积约 4KB,属于可直接打开阅读、按需改造的轻量单页示例。目前已有 257 人学习,说明该方向有一定实用需求。示例围绕获取可视区域尺寸与滚动位置、创建离屏 Canvas、将画布内容通过 toDataURL 转为 Base64 图像地址、动态生成带 download 属性的 a 标签并触发 click 完成保存这一完整流程展开,同时保留了对跨域图片无法导出、复杂 CSS 效果难以还原等限制的说明,便于读者理解原生方案的能力边界与引入 html2canvas 等库的取舍,从而快速搭建可运行的截图下载雏形并迁移到自有项目。
1. 从一张要下载的报表说起:原生 Canvas 截图的适用边界
原生 JavaScript 配合 Canvas 做截图,再用 a 标签下载,听起来像一个小脚本,真正落到业务里却常卡在样式丢失、跨域图片空白和移动端点击没反应上。它适合后台报表导出、活动页分享图、可视化看板留档这类场景:页面结构可控、图片资源能内联、不想引入大型截图库。常见做法是先把 DOM 序列化成自包含 SVG,再画进 Canvas,最后用 Blob 和 a 标签触发下载。这条链路不复杂,但参数设错就会翻车,下面按可复现的顺序拆开写。
2. 把 DOM 变成可绘制像素:原生 JavaScript 下 foreignObject 序列化的关键步骤
2.1 为什么选 SVG foreignObject 而不是逐节点画 Canvas
逐节点画 Canvas 的思路是遍历 DOM,自己算每个元素的坐标、字体、边框和换行,再调fillRect、fillText一点点画。它适合结构固定的图表卡片,遇到表格、弹性布局、文字自动换行就会变成体力活。原生 JavaScript 截图更稳妥的路线是借助 SVG 的foreignObject:把克隆后的 HTML 塞进 SVG,让浏览器按原有布局排版,再把整个 SVG 当成图片画进 Canvas。这样布局计算交给渲染引擎,脚本只负责样式内联和资源内联。
这个方案不是没有边界。foreignObject里的 HTML 必须尽量自包含,外链样式表、外链图片、图标字体、伪元素都可能让截图缺块。它也不适合截取包含视频、iframe 或复杂 Canvas 动画的区域,这些内容在序列化后往往只剩空白。选型时先判断页面是不是“静态结构 + 图片 + 文字”,如果是,这条链路足够;如果页面强依赖实时脚本或第三方嵌入,优先考虑后端排版或让业务方提供专门的分享图模板。
第一步是把目标节点克隆出来,并且把计算后的样式写到克隆节点上。只复制 class 不够,因为页面样式可能来自多层选择器、媒体查询或运行时注入。下面这段代码保留影响布局和视觉的属性,递归处理子节点。
// 只保留影响布局和视觉的属性,避免把无关样式全部写进内联 const KEEP_STYLE = [ 'display', 'position', 'width', 'height', 'margin', 'padding', 'color', 'background-color', 'border', 'border-radius', 'font-family', 'font-size', 'font-weight', 'line-height', 'text-align', 'letter-spacing', 'white-space', 'box-sizing', 'flex', 'flex-direction', 'justify-content', 'align-items', 'grid-template-columns', 'gap', 'opacity', 'overflow' ]; function inlineComputedStyle(source, target) { if (source.nodeType === Node.ELEMENT_NODE) { const cs = getComputedStyle(source); for (const key of KEEP_STYLE) { const value = cs.getPropertyValue(key); if (value) target.style.setProperty(key, value); } // fixed 元素在 foreignObject 里容易偏移,统一转成 absolute if (cs.position === 'fixed') { target.style.position = 'absolute'; } } const srcChildren = source.children; const dstChildren = target.children; for (let i = 0; i < srcChildren.length; i++) { inlineComputedStyle(srcChildren[i], dstChildren[i]); } }KEEP_STYLE不是越全越好。把transform、animation、filter全量复制,容易在 SVG 里出现坐标偏移或渲染差异;如果页面依赖 transform 做居中,建议在克隆后把外层容器的 transform 清掉,改用 margin 或 flex 对齐。position: fixed转absolute是血泪经验,固定定位的工具栏在截图里经常跑到错误位置。递归时要求源节点和克隆节点结构一致,所以克隆要在替换图片之前完成,否则子节点数量会对不上。
还有一个细节:foreignObject里的 HTML 最好加上 XHTML 命名空间。部分内核在序列化后会把没有命名空间的节点当作文本处理,表现是截图一片空白。常见做法是在克隆根节点上设置clone.setAttribute('xmlns', 'http://www.w3.org/1999/xhtml'),再把它塞进foreignObject。这个属性不会影响页面样式,但能减少兼容性玄学。
2.2 图片、字体与伪元素的内联处理
页面里的图片是截图失败的高发区。foreignObject序列化时不会自动把<img>的二进制内容写进 SVG,如果src还是外链,画进 Canvas 时要么缺图,要么因为跨域把画布标记成不可导出。常见做法是把图片转成 dataURL,再替换克隆节点里的src。同源图片可以直接fetch,跨域图片要求资源响应允许跨域读取;如果拿不到,就让服务端直接输出内联数据,或者把图片放到同源静态资源路径下。
async function imageToDataURL(src) { // 同源或允许跨域的资源才能走这条链路 const response = await fetch(src, { mode: 'cors', credentials: 'omit' }); if (!response.ok) { throw new Error(`图片读取失败: ${response.status}`); } const blob = await response.blob(); return await new Promise((resolve, reject) => { const reader = new FileReader(); reader.onload = () => resolve(reader.result); reader.onerror = reject; reader.readAsDataURL(blob); }); } async function inlineImages(root) { const images = [...root.querySelectorAll('img')]; await Promise.all(images.map(async (img) => { if (!img.complete) { await new Promise((resolve) => { img.onload = img.onerror = resolve; }); } const src = img.currentSrc || img.src; if (src && !src.startsWith('data:')) { img.src = await imageToDataURL(src); } })); }mode: 'cors'表示按跨域规则读取,credentials: 'omit'避免携带凭据导致响应头不匹配。img.currentSrc用来处理srcset,否则可能拿到低分辨率图。inlineImages要作用在克隆节点上,不要直接改原页面,否则用户会看到图片闪烁。图片加载失败时不要静默跳过,至少打一条日志,否则导出后才发现缺图,排查成本更高。
字体比图片更隐蔽。document.fonts.ready只能表示字体加载完成,不代表图标字体在 SVG 里可用。如果页面用了自定义字体,最好把字体文件也转成 dataURL,写进一段@font-face样式,再塞进克隆节点。图标字体建议直接换成 SVG 图标,避免字体子集缺失导致方框。伪元素::before、::after不会被cloneNode复制,依赖伪元素画的角标、分割线、装饰图形要么改成真实节点,要么在截图前隐藏。下面这段等待逻辑放在克隆之前,确保图片和字体都已就绪。
async function waitForAssets(root) { // 字体加载完成再克隆,避免文字换行位置变化 await document.fonts.ready; const images = [...root.querySelectorAll('img')]; await Promise.all(images.map(async (img) => { if (img.complete) return; await new Promise((resolve) => { img.onload = img.onerror = resolve; }); })); }waitForAssets只做等待,不替换资源;替换放在克隆后用inlineImages处理。这样原页面不受影响,克隆节点也能拿到已经加载完成的图片尺寸。注意document.fonts.ready在部分内核里返回很快,但字体真正参与布局可能晚一帧,稳妥做法是在它之后再等一个requestAnimationFrame。如果页面有懒加载图片,先触发滚动或手动调用加载逻辑,否则截图里只会看到占位图。
2.3 生成 SVG 字符串并画进 Canvas
样式和图片就绪后,就可以构建 SVG。核心是创建一个svg元素,设置和截图区域一致的宽高与viewBox,再把克隆节点放进foreignObject。序列化时用XMLSerializer,生成 Blob 后通过URL.createObjectURL加载成Image,最后drawImage到 Canvas。这里的宽高单位是 CSS 逻辑像素,不要和 Canvas 的物理像素混在一起。
const SVG_NS = 'http://www.w3.org/2000/svg'; function buildSvgBlob(root, width, height) { const clone = root.cloneNode(true); inlineComputedStyle(root, clone); clone.style.margin = '0'; clone.style.transform = 'none'; clone.setAttribute('xmlns', 'http://www.w3.org/1999/xhtml'); const svg = document.createElementNS(SVG_NS, 'svg'); svg.setAttribute('xmlns', SVG_NS); svg.setAttribute('width', String(width)); svg.setAttribute('height', String(height)); svg.setAttribute('viewBox', `0 0 ${width} ${height}`); const fo = document.createElementNS(SVG_NS, 'foreignObject'); fo.setAttribute('width', '100%'); fo.setAttribute('height', '100%'); fo.appendChild(clone); svg.appendChild(fo); const xml = new XMLSerializer().serializeToString(svg); return new Blob([xml], { type: 'image/svg+xml;charset=utf-8' }); } async function svgBlobToImage(blob) { const url = URL.createObjectURL(blob); const img = new Image(); img.src = url; await img.decode(); URL.revokeObjectURL(url); return img; }width和height用getBoundingClientRect()取整,不要用offsetWidth混用小数,否则viewBox和 Canvas 尺寸会对不齐。clone.style.transform = 'none'是为了去掉父级缩放,截图区域如果本身有缩放动画,先等动画结束。img.decode()比监听onload更直接,能确保图片解码完成再画。URL.revokeObjectURL要在decode之后调用,否则部分内核会把图片资源提前释放,表现是画布空白。
提示:
foreignObject里的内容越简单越稳,外链样式、远程字体、伪元素装饰能去掉就去掉,别把整页复杂布局直接往里塞。
画进 Canvas 时,先创建画布再设置尺寸,最后drawImage。Canvas 的宽高一旦重新赋值,上下文状态会被重置,所以scale、fillStyle这些要放在设置尺寸之后。下一章会展开像素比和导出参数,这里先把绘制链路跑通。
3. 用 a 标签触发下载:Canvas 截图导出时 Blob 与文件名的参数细节
3.1 toBlob 与 toDataURL:格式、质量和异步
Canvas 导出有两种常见方式:toDataURL返回 Base64 字符串,toBlob异步返回 Blob。小图预览用toDataURL省事,但 Base64 会让体积膨胀约三分之一,长图导出时字符串拼接和内存占用都很明显。下载场景优先用toBlob,再配合URL.createObjectURL生成临时地址。toBlob是异步回调,不能像toDataURL那样直接拿返回值,封装成 Promise 更好用。
function canvasToBlob(canvas, type = 'image/png', quality = 0.92) { return new Promise((resolve, reject) => { canvas.toBlob((blob) => { if (blob) { resolve(blob); } else { reject(new Error('Canvas 导出失败,可能是尺寸超限或画布被污染')); } }, type, quality); }); }type常用image/png、image/jpeg、image/webp,不同内核支持程度不一样,落地时以 PNG 为兜底。quality只对 JPEG 和 WebP 生效,PNG 会忽略这个参数;JPEG 常用 0.8 到 0.92,再低文字边缘会糊。回调里blob为空通常有两个原因:Canvas 面积超过设备上限,或者画布里绘制过跨域资源导致不可导出。报错信息要带上这两个方向,排查时先看控制台有没有画布安全错误,再看画布宽高是不是大得离谱。
如果业务必须拿字符串,比如塞进隐藏输入域或走接口上传,toDataURL也能用,但要控制尺寸。常见做法是先用toBlob导出,再用FileReader转 dataURL,这样至少能复用同一份二进制结果。不要对同一张长图反复调用toDataURL,每次都会生成新的大字符串,内存回收跟不上就会卡死。
3.2 a 标签 download 的兼容写法与文件名
下载的核心是创建一个隐藏的a元素,设置href为 Blob URL,download为文件名,插入文档后触发click(),最后移除并释放 URL。download属性在桌面端 Chromium 内核里表现比较直接,部分移动端 WebKit 内核支持不完整,点击后可能只打开图片而不下载。遇到这种情况,常见兜底是检测能力后改为新窗口打开 Blob URL,提示用户长按保存;或者让服务端返回带附件头的下载地址。
function downloadBlob(blob, filename) { const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = filename.replace(/[\\/:*?"<>|]/g, '_'); a.rel = 'noopener'; a.style.display = 'none'; document.body.appendChild(a); a.click(); a.remove(); // 延迟释放,避免部分内核还没开始读取就失效 setTimeout(() => URL.revokeObjectURL(url), 1000); }文件名过滤掉\ / : * ? " < > |这些字符,否则部分系统会截断或直接失败。后缀要和 MIME 对上,image/png配.png,image/jpeg配.jpg,不要出现 Blob 是 PNG 但文件名写.jpg的情况。rel="noopener"是习惯性写法,避免新窗口拿到window.opener。延迟释放 URL 不是必须,但能减少“点了没反应”的玄学问题;如果页面频繁导出,延迟时间不要设太长,否则临时 URL 会堆积。
移动端还有一个细节:a.click()必须发生在用户手势的调用链里。如果前面await了太多异步操作,手势上下文可能丢失,点击会被拦截。常见做法是先同步打开一个空白窗口或显示加载态,等 Blob 生成后再把地址塞进去;或者直接提示用户“生成后长按保存”。不要指望所有移动端浏览器都按桌面逻辑处理download。
3.3 内存与 URL 生命周期
截图导出是内存敏感操作。一张 3000×4000 的 Canvas,按 4 字节/像素算就是 48MB 左右,再叠加 SVG 字符串、Blob、Base64 字符串和临时 URL,很容易把移动端内存打满。每导出一张图,至少释放三样东西:SVG 的 Object URL、下载用的 Object URL、Canvas 的宽高。Canvas 宽高置 0 能尽快让内核回收绘图缓冲区,比只把变量置 null 更直接。
| 方式 | 返回值 | 体积特征 | 适用场景 |
|---|---|---|---|
toBlob | Blob,异步 | 接近原始二进制 | 大图下载、上传 |
toDataURL | Base64 字符串 | 约膨胀三分之一 | 小图预览、内联展示 |
表格里的体积特征是经验值,实际和图片内容有关。纯色背景的 PNG 压缩率很高,复杂渐变和文字多的截图会明显变大。导出前先评估目标区域尺寸,超过常见阈值就降像素比或分片处理,不要等浏览器白屏了再补。
URL.revokeObjectURL的调用时机要跟下载节奏配合。桌面端点击后立即释放通常也能下载,但部分内核开始读取 Blob 有延迟,释放太早会得到空文件。延迟 1000ms 是折中值;如果文件很大,可以监听a的点击后再等一会儿。更好的做法是把下载封装成“生成 Blob、触发下载、延迟释放”三步,调用方只关心文件名和格式。
4. 截图质量与像素比:Canvas 截图里 devicePixelRatio 与缩放怎么设
4.1 逻辑像素、物理像素与 Canvas 尺寸
页面布局用的是 CSS 逻辑像素,屏幕实际渲染用的是物理像素,两者之间隔着devicePixelRatio。如果 Canvas 宽高直接等于getBoundingClientRect()的宽高,导出图在高分屏上会显得发虚,文字边缘不够锐利。常见做法是把 Canvas 实际宽高乘以像素比,再用ctx.scale(dpr, dpr)把绘制坐标拉回逻辑像素。这样代码里仍然按 CSS 尺寸画,输出图却拿到了更多像素。
function createSnapshotCanvas(width, height, maxDpr = 2) { const dpr = Math.min(window.devicePixelRatio || 1, maxDpr); const canvas = document.createElement('canvas'); canvas.width = Math.round(width * dpr); canvas.height = Math.round(height * dpr); const ctx = canvas.getContext('2d'); ctx.scale(dpr, dpr); ctx.imageSmoothingEnabled = true; ctx.imageSmoothingQuality = 'high'; return { canvas, ctx, dpr }; }width和height传逻辑像素,maxDpr是上限,常用 2 或 3。不要直接放开到设备真实像素比,某些移动设备报告 3 甚至更高,长图面积会翻几倍。ctx.scale之后,drawImage(img, 0, 0, width, height)仍然按逻辑尺寸画,不需要手动乘 dpr。imageSmoothingQuality在部分内核里不生效,但有比没有好;如果导出的是文字为主的长图,可以先用 2 倍 dpr 试,再根据文件体积降级。
像素比不是越高越好。导出图最终是给人看还是给机器识别,决定参数取向:分享图 2 倍足够,打印物料才考虑 3 倍;后台留档如果只是存档,1.5 倍甚至 1 倍都能接受。把 dpr 做成可配参数,别写死在函数里,后面调优会省很多事。
4.2 长图截取的分段绘制与尺寸上限
长报表、长列表截图最容易遇到 Canvas 尺寸上限。不同内核的上限不一样,常见做法是把单边控制在 4096 到 8192 像素以内,超过就降 dpr 或分段导出。分段绘制的思路是把 SVG 图片按纵向切成若干块,每块画进同一个 Canvas,每画完一块让出一帧,降低主线程长时间阻塞。它不能突破 Canvas 总尺寸上限,但能减少瞬时内存压力,也能让页面在导出过程中保持响应。
async function drawSvgInTiles(ctx, img, width, height, tileHeight = 2048) { for (let y = 0; y < height; y += tileHeight) { const h = Math.min(tileHeight, height - y); ctx.drawImage(img, 0, y, width, h, 0, y, width, h); // 每块让出一帧,避免长任务卡死页面 await new Promise((resolve) => requestAnimationFrame(resolve)); } }drawImage九参数版本里,前四个是源图裁剪区域,后四个是目标区域。tileHeight按设备内存调,桌面端可以 2048 到 4096,移动端建议 1024 到 2048。分段绘制时目标 Canvas 总高度仍然是完整高度,所以总尺寸上限还在;如果总高度超过上限,优先降 dpr,再把最终图缩放回目标尺寸。不要为了追求清晰度硬扛,导出失败比稍微糊一点更影响使用。
如果业务允许,长图可以拆成多张下载,比如每 5000 像素一张,文件名加序号。这样单张图不超限,用户也好打开。拆图时注意重叠区域,避免表格行被切断;常见做法是按固定高度切,但在切点附近找最近的分隔线或行边界,找不到就保留固定切点。
4.3 导出质量与文件体积的平衡
PNG 无损、支持透明,适合文字、表格、带透明背景的分享图;JPEG 体积小,但不支持透明,文字边缘容易出现压缩噪点;WebP 体积和质量的平衡更好,但部分老内核不支持导出。截图里如果有大面积纯色背景,PNG 往往比 JPEG 更小;如果是照片或复杂渐变,JPEG 0.85 左右更合适。导出前先判断目标区域有没有透明背景,有就选 PNG,没有可以试 JPEG。
JPEG 导出前要先铺底色。Canvas 默认是透明黑,直接导 JPEG 会把透明区域变成黑色,看起来像截图坏了。常见做法是在drawImage之前用白色填充整个画布,再画内容。如果设计稿要求其他底色,把颜色做成参数传进去。
function prepareJpegBackground(ctx, width, height, color = '#ffffff') { ctx.save(); ctx.fillStyle = color; ctx.fillRect(0, 0, width, height); ctx.restore(); }ctx.save()和restore()包住填充,避免影响后续绘制状态。width、height用逻辑像素,因为上下文已经按 dpr 缩放过。填充要在画 SVG 之前,否则会把内容盖住。这个细节看起来小,但 JPEG 导出黑底是高频翻车点,尤其是分享图带圆角或透明边距时。
注意:导出格式要和下载文件后缀一致,JPEG 不铺底会得到黑底图,PNG 强行铺底又会丢掉透明区域,先确认业务要不要透明。
5. 避坑与排查:五个真实翻车现场
5.1 截图里图片是空白方块
现象:页面预览正常,导出的 PNG 里头像、商品图、图表全部变成空白,控制台没有明显报错。原因通常是克隆节点里的<img>还是外链地址,foreignObject序列化后没有把图片二进制带进去;或者图片走了srcset,克隆后取到的地址和当前显示的不是同一张。解决分两步:先在克隆后执行inlineImages,把所有img转成 dataURL;再检查currentSrc,不要只读src。跨域图片拿不到时,让服务端直接输出内联数据,或者把资源放到同源静态路径下,别在导出时才发现读不了。
还有一种情况是图片加载完成但尺寸为 0。页面用 CSS 给图片设了宽高,克隆节点只复制了width、height属性,没有复制计算样式,SVG 里图片就按原始尺寸撑开或塌缩。解决是把width、height、object-fit一起写进内联样式,并在替换src后等一次decode()。如果图片是背景图,foreignObject对内联背景图的支持更差,能改成<img>就改,改不了就在截图前把背景图区域单独绘制。
5.2 Canvas 被污染导致 toBlob 抛错
现象:调用toBlob时回调收到null,控制台出现画布安全错误,提示无法导出。原因是 Canvas 里绘制过跨域资源,且该资源没有允许跨域读取;SVG 里引用了外部 URL 也会触发同样问题。解决是保证进入 Canvas 的所有图片都是 dataURL 或同源 Blob URL,fetch图片时带上mode: 'cors',并确认响应头允许当前页面读取。只在<img>上设置crossOrigin='anonymous'不够,资源端不配合照样污染。
排查时先把 SVG 字符串打印出来,搜索http开头的src和href,有一个就说明没内联干净。样式里的background-image: url(...)也要查,很多人只处理了<img>,忘了背景图。实在拿不到跨域资源,就在截图前把该区域替换成占位图,或者让业务方提供可导出的图片地址。不要试图用toDataURL绕过,画布一旦被污染,两种导出方式都会失败。
5.3 移动端点击下载没反应
现象:桌面端点按钮正常下载,移动端点完没有任何反应,或者只在新窗口打开图片。原因是部分移动端 WebKit 内核对download属性支持不完整,Blob URL 在新窗口里也可能被拦截。解决是先做能力检测,支持download就走 a 标签下载;不支持就打开图片并提示长按保存,或者调用系统分享能力把文件交给用户。不要在异步链里隔太久才触发点击,用户手势上下文丢失后,新窗口会被拦截。
常见兜底是生成 Blob 后先显示一个预览层,把图片放进去,让用户长按保存。这个方案体验不如直接下载,但比点击没反应好。另一个方向是让服务端返回带附件头的下载地址,前端只负责跳转;这条链路适合文件较大或格式固定的场景。无论哪种方案,都要在真机测,不要只看桌面模拟器。
5.4 字体错乱、行高偏移
现象:截图里的文字换行位置和页面不一致,标题被截断,图标字体变成方框。原因是foreignObject里的字体没有随 SVG 一起内联,或者字体还没加载完就克隆了节点。解决是等待document.fonts.ready,再把字体文件转成 dataURL 写进@font-face;至少要把font-family、font-size、line-height、letter-spacing复制到克隆节点。图标字体尽量换成 SVG,字体子集缺字时很难靠样式补救。
行高偏移通常和box-sizing、line-height的继承有关。页面里用了line-height: 1.5,克隆节点如果只复制了font-size没复制line-height,文字占位就会变。解决是把line-height计算后的值一起写进内联样式。表格场景还要注意border-collapse和table-layout,缺一个就可能出现列宽错位。
5.5 大页面截图内存暴涨或直接白屏
现象:长报表导出时页面卡死,进度条走完后 Canvas 变成空白,或者浏览器直接提示页面无响应。原因是 Canvas 面积超过设备上限,或者同时保留了 SVG 字符串、Blob、dataURL 和多个临时 URL。解决是限制maxDpr,长图分段绘制,导出后把 Canvas 宽高置 0,并及时revokeObjectURL。不要在一个页面里连续导出多张长图,中间至少让用户触发一次或强制等待一帧。
排查时先看画布尺寸,宽高乘起来超过几千万像素就要警惕。再看是否重复创建 Canvas,每次导出都新建一个且不释放,内存会一路涨。常见做法是复用同一个离屏 Canvas,导出完成后清空宽高;如果业务要求并发导出,给并发数设上限,比如同时只跑一个任务。白屏不一定是代码错,很多时候就是内存到顶了。
6. 把截图做成可复用能力:封装 downloadSnapshot 与验证清单
把前面的步骤串成一个函数,调用方只需要传根节点、文件名和少量参数。下面这版是精简封装,依赖前文的inlineComputedStyle、inlineImages、buildSvgBlob、createSnapshotCanvas、canvasToBlob、downloadBlob,实际项目里把这些函数放在同一个模块即可。
async function downloadSnapshot(root, filename = 'snapshot.png', options = {}) { const { type = 'image/png', quality = 0.92, maxDpr = 2 } = options; const rect = root.getBoundingClientRect(); const width = Math.ceil(rect.width); const height = Math.ceil(rect.height); await document.fonts.ready; const clone = root.cloneNode(true); inlineComputedStyle(root, clone); await inlineImages(clone); const svgBlob = buildSvgBlob(clone, width, height); const svgUrl = URL.createObjectURL(svgBlob); const img = new Image(); img.src = svgUrl; await img.decode(); const { canvas, ctx } = createSnapshotCanvas(width, height, maxDpr); if (type === 'image/jpeg') { ctx.fillStyle = '#ffffff'; ctx.fillRect(0, 0, width, height); } ctx.drawImage(img, 0, 0, width, height); URL.revokeObjectURL(svgUrl); const blob = await canvasToBlob(canvas, type, quality); downloadBlob(blob, filename); canvas.width = canvas.height = 0; }| 参数 | 类型 | 说明 |
|---|---|---|
root | HTMLElement | 要截图的根节点,宽高由getBoundingClientRect决定 |
filename | string | 下载文件名,后缀和type保持一致 |
type | string | 默认image/png,照片类内容可换image/jpeg |
quality | number | 0 到 1,仅 JPEG/WebP 生效,常用 0.85 到 0.92 |
maxDpr | number | 像素比上限,移动端建议 2,长图可降到 1.5 |
调用时先确保根节点在视口内或至少已完成布局,隐藏区域要先临时显示再截图。验证清单我一般跑三步:先截一个纯文字卡片,看字体和换行;再截一个带跨域图片的卡片,看是否空白或报安全错误;最后在移动端真机点一次下载,看是否触发保存或需要长按。三步都过,再上长图和 JPEG 格式。
从那以后我每次接截图需求,都先把跨域图片、字体加载、移动端下载这三件事写成检查项,跑不通就不往下写。希望帮到你。
本文还有配套的精品资源,点击获取