去年做企业报表系统的时候,我被"把HTML页面导出成PDF"这个需求折磨得不轻。第一版导出的PDF,字是虚的,像隔了一层磨砂玻璃;第二版倒是清晰了,但表格右边被硬生生切掉一截;第三版更离谱,所有图片直接白屏。当时整个人是崩溃的。后来静下心把 html2canvas + jsPDF 这条链路的原理彻底捋了一遍,才发现这三大坑背后其实是对canvas绘制机制和PDF页面坐标体系的误解。这篇就把我踩完坑之后的完整方案和为什么这么写的底层逻辑一次性讲清楚。
1. 为什么HTML转PDF总在"能出文件"和"能用文件"之间翻车
先别急着贴代码,我们得先搞清楚一件事:所谓"HTML转PDF",在前端领域从来都不是真的在解析HTML然后重新排版成PDF。主流的 html2canvas + jsPDF 方案,本质是把DOM"拍一张照"——先由html2canvas把页面截成一张canvas位图,再把这张图片塞进jsPDF生成的多页PDF里。听起来简单,但所有坑都藏在这个"拍照"和"塞图"的过程中。
先说清晰度问题。html2canvas有一个 scale 参数,默认值是1。这个值代表canvas内部像素和设备像素之间的缩放比例。如果屏幕是2倍屏(devicePixelRatio=2), scale=1 意味着你截图时放弃了一半的物理像素,只在canvas里画了一份逻辑像素数据。等pdf.js把这张图渲染出来,相当于把一张缩小图强行放大到实际尺寸,自然就是糊的。这个问题的本质是:你截图时的"采样精度"远低于显示器的物理精度。
再说页边距问题。jsPDF的坐标系和CSS的坐标系完全是两套逻辑。CSS里一个div的宽高、margin都以像素为基准,而jsPDF默认以毫米为基准,按照PDF标准分辨率(96dpi)换算,也就是1毫米约等于3.78像素。很多人直接把DOM高度交给 addImage ,图片按比例缩放后生成的PDF,上下左右完全没有留白,内容顶到纸的边缘,打印机一打就裁掉。更关键的是,html2canvas截图时如果页面存在滚动容器,它截取的宽度和高度可能比视口大得多,直接导致分页时表格行被拦腰截断。
最后是图片跨域。html2canvas绘制图片时,canvas会被浏览器的安全策略标记为"被污染"(tainted canvas),一旦检测到跨域图片且没有正确的CORS头,canvas的 toDataURL 方法就会抛出 Tainted canvases may not be exported 异常,图片区域直接画不上。很多项目里图片CDN域名和业务域名不是同一个,于是导出PDF时图片全部空白。这个问题在本地开发环境往往发现不了,因为本地没有跨域,等部署到测试环境才爆雷。
理解了这三个根源,后面的解决方案就不是瞎试了。下面按三条线分别拆解,最后再给出一个可以直接抄进项目的完整组件。
2. 清晰度问题:scale参数、devicePixelRatio和canvas的那本像素账
清晰度是所有问题里最直观的,也是最容易解决的。但如果你只是把网上搜到的 scale: 2 抄进代码,遇到高清屏导出依然可能出问题,因为没搞清这个2是怎么来的。
2.1 先弄明白一份PDF图片的分辨率是怎么算出来的
假设页面上一个内容区域的CSS宽度是190mm,换算成像素大约是 190 * 3.78 ≈ 718px 。如果你用 scale: 1 截图,生成的就是一张718px宽的位图。把这张图放到PDF的190mm宽度里,每毫米只分配了约3.78个像素,换算成印刷行业常说的dpi就是96。打印机的常规要求是300dpi,145dpi以上才能勉强达到"视觉清晰",96dpi就只能算"屏幕上凑合看"。
而 scale: 2 意味着canvas以两倍的像素密度进行采样,718px的CSS宽度会生成1436px的位图,放进同样的190mm里,dpi提升到192。如果继续用 scale: window.devicePixelRatio ,在2倍屏上效果等同于scale=2,在3倍屏上是scale=3。但这里有个细节很容易忽略:截图会把整个DOM内容放大,包括文字。canvas是一个位图容器,里面的所有元素都会被放大绘制,如果你在截图容器上设置了固定的CSS宽度,那么2倍scale相当于在绘制时把所有内容拉伸到2倍尺寸,文字会变清晰,但也意味着内存占用翻4倍(宽和高各翻一倍)。
2.2 实操中的清晰度参数该怎么定
我的实践是,导出A4横纵通用场景下, scale 取 Math.min(window.devicePixelRatio, 3) 比较稳。为什么封顶3?因为超过3之后,肉眼几乎分辨不出差异,但浏览器的canvas内存直接爆炸。曾经测试过一张结构复杂的表单单页截图,scale=4时在低端安卓机上内存占用飙到400MB,页面直接卡死;scale=3时大约180MB,属于可以接受的范围。
async function captureToCanvas(element, scale) {
const canvas = await html2canvas(element, {
scale: scale || Math.min(window.devicePixelRatio, 3),
useCORS: true,
logging: false,
backgroundColor: '#ffffff',
// 关键:留出足够的绘制空间,防止内容被裁切
windowWidth: element.scrollWidth,
windowHeight: element.scrollHeight
});
return canvas;
}
windowWidth 是另一个关键参数。如果页面外层有 overflow: auto 的滚动容器,html2canvas默认只按视口尺寸绘制,超出部分会被裁掉。把 windowWidth 和 windowHeight 设为元素的scroll宽高,是解决"截图不全"的常规做法。
2.3 顺带把"导出表格断行"的苗头也解决掉
清晰度问题解决后,紧接着会遇到表格被截断。这段在讲清晰度章节里必须提醒:如果表格很长,你直接截一整张长图塞进PDF,分页时jsPDF会按页面高度均分切割,很可能把某一行文字切掉一半。我的做法是:对表格这类结构,不截整个页面,而是先判断内容高度是否超过单页可容纳高度,如果超过,就用canvas的 getContext('2d').getImageData 手动按页高切片,一页一页生成。这个逻辑在下一页会细说,但那段的思路和清晰度定位不同——那段的重点是
3266




被折叠的 条评论
为什么被折叠?



