ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

html2canvas+jsPDF实现HTML转PDF:中文、图片、表格与分页切割完整指南

html2canvas+jsPDF实现HTML转PDF:中文、图片、表格与分页切割完整指南 简介针对需要将HTML页面转为PDF的Web前端开发者这份源码包提供了一套基于jsPDF的免插件实现方案无需安装任何浏览器插件即可在纯前端环境中完成转换并原生支持中文、图片与表格的渲染。压缩包共10个文件以5个JavaScript脚本为核心另含2个HTML演示页面、1个CSS样式文件、1个中文字体TTF以及1张示意图片整体仅1.76MB结构清楚便于按需提取或移植到现有项目。包内已内置转换好的中文字体和字体转换工具只需6行代码就能将任意网页对象原样矢量输出为PDF达到所见即所得的效果特别适合报表导出、网页存档、合同预览等场景。目前已有527人学习下载兼具参考与实用价值可直接作为前端PDF生成功能的起点。1. HTML转PDF的另一种思路html2canvas加jsPDF不装插件也能还原页面HTML转PDF这个需求做后台管理系统的前端几乎都躲不开。合同打印、报表存档、工单导出用户永远希望“页面长什么样PDF就长什么样”。对比几种常见路线之后你会发现纯前端用html2canvas把DOM截成canvas再用jsPDF把canvas输出成PDF是投入产出比最高的不需要浏览器插件不需要额外部署服务CSS3布局还原度也比预渲染服务稳定。中文、图片、表格这些“劝退项”确实存在但只要把字体加载、跨域图片、分页切割这三件事处理干净这套方案能覆盖九成以上的导出场景。下面的主线代码可以直接抄关键参数和踩坑位置都标出来了。适合被业务频繁要求导PDF的前端也适合正在做导出功能技术选型的人。2. 把DOM变成PDF核心链路与最小可跑通的完整代码2.1 为什么绕开插件和后端渲染在动手之前先把路线选型说透。window.print()依赖浏览器打印对话框用户可以另存为PDF但页面要额外写media print样式打印预览的体验不可控而且没法在应用里直接触发文件下载。Puppeteer这类无头浏览器方案还原度最高但要维护一套Chromium环境Linux服务器上还容易缺中文字体。jsPDF纯手工绘制表格和图片工作量相当于自己重新实现一遍页面渲染器只适合极简单的版式。html2canvas截图方案的好处是排版交给浏览器本身完成页面已经渲染成什么样截出来就是什么样改造量被压缩到“加一个导出函数”。方案还原度依赖体积适用场景短板window.print()中零打印预览、浏览器原生导出体验不可控需要写打印样式Puppeteer高大服务端批量生成部署重中文字体易踩坑jsPDF手绘低小固定版式简单单据表格图片排版工作量巨大html2canvasjsPDF高两个JS库前端页面直接导出输出是图片型PDF文字不可选中这个对比表不是绝对的但如果你没有批量导出和高安全诉求“两个JS库搞定”基本就是最优解。2.2 最小可跑通的完整代码先搭一个最基础的页面把两个库用CDN引进来。html2canvas负责截图jsPDF负责生成PDF两者职责分开后面所有坑都能归到“截图阶段”或“导出阶段”。!DOCTYPE html html langzh-cn head meta charsetutf-8 titlehtml转pdf示例/title script srchttps://cdn.jsdelivr.net/npm/html2canvas1.4.1/dist/html2canvas.min.js/script script srchttps://cdn.jsdelivr.net/npm/jspdf2.5.1/dist/jspdf.umd.min.js/script /head body div idpageContent h2项目验收单/h2 p这是一个可以直接导出的测试页面。/p /div button onclickhtmlToPdf(pageContent, 验收单)导出PDF/button /body /htmlasync function htmlToPdf(selector, fileName) { const element document.querySelector(selector); const canvas await html2canvas(element, { scale: 2, // 采样倍数2倍能让文字和表格线更清晰 useCORS: true, // 允许跨域图片参与绘制 backgroundColor: #ffffff, // 透明背景导出后容易发黑显式给白色最稳 logging: false, // 关掉调试日志控制台干净一点 windowHeight: element.scrollHeight // 整页截图而不是只截首屏 }); const imgData canvas.toDataURL(image/jpeg, 0.92); const pdf new jsPDF(p, mm, a4); pdf.addImage(imgData, JPEG, 0, 0, 210, 297); pdf.save(fileName .pdf); }这段代码能直接跑的前提是页面内容不超过一页A4纸一旦超过addImage会把下半部分截掉这是后面章节要解决的切页问题。先看参数scale是采样倍数1倍导出时文字边缘发虚2倍明显更实但canvas的内存占用会按宽高各乘2变成4倍移动端尤其敏感。useCORS打开后html2canvas会尝试以跨域方式读取图片但能不能真正读到像素还取决于图片服务器返回的Access-Control-Allow-Origin头这条是图片问题最集中的坑。backgroundColor默认其实是白色但遇到页面有透明背景时会渲染成黑色所以显式设置最保险。2.3 一份带表格和图片的真实页面把测试页面换成更接近业务的样子加上一张跨域图片和一个表格再跑一次导出。这段代码里的页面结构可以直接复用重点是表格样式尽量写在行内后面讲表格坑的时候会解释原因。div idreport h3设备巡检记录/h3 img srchttps://img.example.com/device.jpg alt设备照片 crossoriginanonymous table classreport-table stylewidth:100%;border-collapse:collapse;font-size:14px; thead tr stylebackground:#f3f3f3; th styleborder:1px solid #ccc;padding:6px;巡检项/th th styleborder:1px solid #ccc;padding:6px;结果/th /tr /thead tbody tr td styleborder:1px solid #ccc;padding:6px;电源状态/td td styleborder:1px solid #ccc;padding:6px;正常/td /tr /tbody /table /div调用方式不变就是第二节里的htmlToPdf(report, 巡检记录)。图片地址换成真实业务地址后如果导出时报SecurityError基本就是图片服务器没开跨域访问这个问题将在第4章展开排查。2.4 一个导出函数的参数速查参数推荐值作用什么情况下调整scale2控制canvas采样分辨率页面宽超过1200px时降到1.5防移动端崩溃useCORStrue允许跨域图片绘制图片服务器支持跨域时必须开启backgroundColor#ffffff输出底色暗色主题页面改成对应底色loggingfalse关闭调试日志排查canvas异常时可临时打开imageQuality0.92JPEG压缩质量导出文件过大时降到0.8formata4输出纸张需要自定义大小时改用[宽, 高]数组这套参数组合是平时用得最多的一套导出效果和性能基本在甜点上。项目跑通之后下面要处理的就是三大高频障碍中文、图片、表格。3. 中文、图片、表格三座大山的逐项拆解3.1 中文显示先解决字体加载时机先说一个容易绕远的结论html2canvas导出的是图片PDF里的中文其实是像素点阵不存在“PDF缺中文字体”的经典问题。很多人一上来就研究jsPDF的addFont、写字体embed脚本那是jsPDF纯文本绘制路线才需要的。真正的坑在截图的瞬间。如果页面上用了自定义字体而字体文件还没加载完canvas截出来的就是系统回退字体严重时会直接显示方块。解决思路是“等字体就绪再截图”。现代浏览器提供了document.fonts.ready这个Promise会等页面所有font-face字体加载完成后resolve。async function waitForFonts() { if (document.fonts document.fonts.ready) { await document.fonts.ready; } } async function htmlToPdf(selector, fileName) { await waitForFonts(); const element document.querySelector(selector); const canvas await html2canvas(element, { scale: 2, useCORS: true, backgroundColor: #ffffff, logging: false, windowHeight: element.scrollHeight }); const imgData canvas.toDataURL(image/jpeg, 0.92); const pdf new jsPDF(p, mm, a4); pdf.addImage(imgData, JPEG, 0, 0, 210, 297); pdf.save(fileName .pdf); }waitForFonts这个函数是关键它处理的是字体加载和截图之间的竞态。如果你用的是系统字体比如微软雅黑、苹方浏览器本地就有不需要等待但业务系统里常见的思源黑体、站酷字体都是网络字体不等待直接截十次有三四次字体不对。另一个细节是字体文件本身也要支持跨域字体CSS里如果引入的是CDN地址CDN必须返回Access-Control-Allow-Origin否则浏览器不会让canvas读取字体的渲染结果。3.2 图片跨域、防盗链与canvas污染图片是html2canvas方案里最主流的翻车现场。业务系统的图片通常存在对象存储或图床上前端页面能显示是因为标签加载图片不受跨域限制但canvas读取图片像素时会执行同源策略一旦图片服务器不允许跨域访问canvas就被“污染”toDataURL直接抛SecurityError。function loadImageAsDataURL(src) { return new Promise((resolve, reject) { const img new Image(); img.crossOrigin anonymous; img.onload () { const c document.createElement(canvas); c.width img.naturalWidth; c.height img.naturalHeight; const ctx c.getContext(2d); ctx.drawImage(img, 0, 0); resolve(c.toDataURL(image/jpeg, 0.9)); }; img.onerror reject; img.src src; }); }这个函数把跨域图片先转成dataURL再塞进页面html2canvas读取的就是本地数据不再受跨域限制。但要注意img.crossOrigin anonymous只是发起跨域请求图片服务器必须返回Access-Control-Allow-Origin头否则onerror会触发连图片都加载不出来。实际项目里最稳妥的做法是后端提供一个图片代理接口前端把img的src统一改成代理URL比如/img-proxy?urlxxx代理服务器转发请求并加上跨域头。这样既绕开防盗链也不依赖图片源站配合修改响应头。前端方案解决不了的用同域代理基本都能兜底。3.3 表格边框丢失、背景色缺失与断行表格是html2canvas另一个高发问题区。细边框丢失是最常见的尤其当表格用了border-collapse: collapse且字号比较小的时候canvas渲染的1px线条会断断续续。把scale调到2或以上边框断裂问题会大幅缓解因为采样分辨率高了线条不再是贴着一个像素渲染。背景色问题则不同。html2canvas会读取计算样式但如果斑马纹背景是写在CSS类里的偶尔会出现整行背景变白的情况。一个省心的做法是给tr或td加内联style让背景色成为元素自身的style属性html2canvas对行内样式的读取优先级最高基本不会丢。断行问题发生在跨页时表格被canvas切割线拦腰切断一行文字的上半部分在第1页下半部分在第2页。这个问题要结合分页切页来解决思路是把切割位置对齐到表格行高第5章会给出具体算法。先记住一个规律切页的位置不能按固定像素硬切要往最近的表格行边界上靠。4. 遇到bug怎么排查五个高频问题从现象到处理4.1 SecurityError和crossorigin属性现象点击导出后控制台报Uncaught DOMException: Failed to execute toDataURL on HTMLCanvasElement: The canvas has been tainted by cross-origin data。原因页面里有跨域图片且图片服务器没有返回Access-Control-Allow-Origincanvas被污染无法输出图片数据。解决把页面所有img元素加crossoriginanonymous同时在图片服务器或代理层加上跨域响应头。能用代理的优先走代理因为部分图片源站会校验Referer只加crossorigin属性会被防盗链拦掉。4.2 导出成功但PDF全白现象PDF文件能正常生成文件名也对打开后整页白纸。原因截图时目标元素还没有完成渲染或者字体、图片还在加载中canvas截到了一张空白页。另一个可能原因是目标元素设置了display:nonehtml2canvas对隐藏元素截出来就是空白。解决导出前await document.fonts.ready图片改成async加载完成后调导出函数并检查目标元素的display状态。最直观的调试方式是先在控制台手动执行一次html2canvas生成canvas用canvas.toDataURL塞到页面里看看内容是否完整这一步能快速定位是截图问题还是PDF输出问题。4.3 只截到了首屏现象页面很长但PDF里只有浏览器可视区域那一部分内容。原因html2canvas默认按视口高度截图不传windowHeight时只截首屏。还有一种情况是页面处于滚动状态window.scrollTo(0, 0)没执行。解决给html2canvas传windowHeight参数值为target.scrollHeight。注意如果页面里有position: fixed的悬浮元素整页截图时该元素会被固定在画布初始位置上可能会覆盖内容。处理方式是先临时把fixed元素隐藏截图完成后再恢复显示。4.4 中文变成方块或字体不对现象导出PDF里中文全部变成豆腐块或者明明设置的是思源黑体导出来却像宋体。原因字体文件没加载完就截图canvas用了系统回退字体来渲染。解决按3.1的做法先把document.fonts.ready await到。如果字体是异步加载的还要进一步判断字体是否真正生效可以用document.fonts.check(16px Source Han Sans SC)来确认。字体加载是玄学次数最多的环节把“先等字体、再截图”写成固定流程能省掉一半问题。4.5 移动端崩溃与PDF文件过大现象手机上点导出浏览器卡住或白屏导出成功的PDF几十页文件上百MB。原因scale设成2倍后canvas内存暴增移动端浏览器受不了同时JPEG质量0.92对照片类图片来说太高文件体积直线上升。解决在移动端把scale降到1.5PDF路径把imageQuality降到0.8。如果页面确实很长不要一次把整页截成一张巨大canvas用第5章的切割方案边切边导峰值内存会小很多。文件大小也要注意图片型PDF体积本来就不小质量参数调到视觉上看不出差异就行。5. 多页分页切割与最终自查一张A4装不下内容时的处理5.1 切片算法canvas按页高切割再输出页面超过一页时正确做法是把canvas截成若干段每段对应一条PDF页面。不要用addImage整张塞进去然后靠负坐标翻页那样第一张图的内存和清晰度都不好平衡。下面的切割是拿canvas的像素坐标来切再按比例换算成毫米。function htmlToPdf(selector, fileName) { const element document.querySelector(selector); html2canvas(element, { scale: 2, useCORS: true, backgroundColor: #ffffff, logging: false, windowHeight: element.scrollHeight }).then((canvas) { const pdfWidth 210; const pdfPageHeight 297; const pxPerMm canvas.width / pdfWidth; // 每毫米对应多少canvas像素 const pagePxHeight pdfPageHeight * pxPerMm; // 每页PDF对应的canvas像素高度 const pdf new jsPDF(p, mm, a4); let y 0; let page 1; while (y canvas.height) { const slicePxHeight Math.min(pagePxHeight, canvas.height - y); const sliceCanvas document.createElement(canvas); sliceCanvas.width canvas.width; sliceCanvas.height slicePxHeight; const ctx sliceCanvas.getContext(2d); ctx.fillStyle #ffffff; ctx.fillRect(0, 0, sliceCanvas.width, sliceCanvas.height); ctx.drawImage(canvas, 0, y, canvas.width, slicePxHeight, 0, 0, canvas.width, slicePxHeight); const pageImg sliceCanvas.toDataURL(image/jpeg, 0.92); if (page 1) pdf.addPage(); pdf.addImage(pageImg, JPEG, 0, 0, pdfWidth, slicePxHeight / pxPerMm); y slicePxHeight; page; } pdf.save(fileName .pdf); }); }逻辑不复杂把大canvas按pagePxHeight切成小段每段放到一个临时canvas上再addImage到当前PDF页面。percent换算统一用pxPerMm不会出现比例变形。这段代码的一个小细节是切割后先填充白底再画图避免JPEG压缩把透明区域压成黑色。5.2 表格跨页对齐把切割线挪到行边界上面的算法在纯文字页面上没问题但遇到表格切割线可能正好穿过一行文字。经验做法是先找表格行元素在canvas里的位置再把切割点调整到最近的行边界。因为scale2时canvas坐标和DOM坐标是2倍关系所以可以在切割前计算行元素的offsetTop。function findSafeCutY(canvas, element, y, pagePxPerHeight) { const scale 2; const rows element.querySelectorAll(tr); for (let offset 8; offset 0; offset--) { const candidate y pagePxPerHeight - offset * scale; for (const row of rows) { const rowY row.offsetTop * scale; if (Math.abs(candidate - rowY) 4 * scale) { return candidate; } } } return y pagePxPerHeight; }这段逻辑是在每页切割线附近找一个离某行边界最近的偏移量最多往上推8行找到就直接用。没有表格的页面直接返回原始切割高度。实际使用中把这个函数塞进5.1循环里替换slicePxHeight的计算表格跨页断字的问题就基本消失了。5.3 导出后的自查清单检查项验证方法有问题时的调整参数中文显示导出后放大查看是否有方块等待document.fonts.ready检查字体CORS图片完整逐页翻看图片是否缺失检查useCORS和代理配置表格边框看细边框是否断裂上调scale至2或2.5表格跨页找跨页行看是否被切断用findSafeCutY对齐行边界文件大小查看PDF体积是否异常下调imageQuality至0.8页面高度翻到末页是否有大片空白微调slicePxHeight或分页阈值这套切页方案放到真实项目里用了大半年让我印象最深的是字体时序问题很多页面刷新后立刻点导出截图就是不对流程图和表格样式都对唯独中文字体是回退字体。从那以后我每次做导出功能强制让导出按钮先走一遍字体就绪再开放点击并且始终保持着“先整页截一张检查再上切割算法”的习惯。多页切割、表格行对齐这套流程我建议你也固定下来能省掉不少抽查PDF的返工时间。希望帮到你。本文还有配套的精品资源点击获取
返回列表