
简介面对浏览器打印时排版错乱、中文乱码、图片丢失等常见痛点这套完整源码提供基于jspdf与html2canvas的轻量化解决方案无需任何浏览器插件即可将任意HTML页面高质量导出为PDF尤其适合中文业务系统。压缩包共10个文件大小仅1.76MB核心包括JavaScript库、两个可直接运行的示例HTML页面、中文字体、字体转换工具及CSS样式各模块用途清晰便于二次开发。资源内置了转换好的中文字体规避中文乱码问题同时附带字体转换工具允许开发者按需生成其他字体仅需6行代码即可实现所见即所得的原样矢量输出完整支持文本、图片与表格元素。已有528人学习下载适合前端开发者、运维人员或需要批量生成报告、合同、票据等场景的工程师快速落地PDF导出功能。1. 为什么说 html 转 pdf 文件下载最合理的方案是浏览器打印搜 html 转 pdf 文件下载常见思路是装打印插件、引一个 JS 绘图库、或者写个后端接口生成文件。我这些年做过的模拟项目X里凡是走这三条的基本都会在两个字上翻车乱码和排版。中文变成方块、表格跨页被拦腰切断、图片加载成空白。换到浏览器自带的打印能力之后这些问题一次性消失了大半chrome、edge、safari 早就内置了 PDF 输出你点一下按钮选“另存为 PDF”一份完整的中文文档就落地了。这个方向叫 web 页面 pdf 打印也好叫 html 转 pdf 完整源码也好核心都是同一件事让浏览器排版引擎把页面当纸张渲染。它不需要任何插件系统字体负责中文网络图片自己加载表格交给打印引擎分页三样全支持。下面的内容从最小实现写起逐步覆盖中文图片表格的处理、PDF 文件名控制以及常见的坑。适合那些不想引入重型依赖、又必须支持中文与图片表格的下载场景。2. 最小可跑的实现window.print() 加 media print一个函数搞定下载2.1 为什么主线是浏览器打印而不是 JS 绘图库、插件或后端服务先回答一个经常被追问的问题市面上明明有专门生成 PDF 的 JS 库为什么还要绕回打印原因很现实。那些库的本质是在 canvas 里把 DOM 重新画一遍画出来的文字是矢量还是图片取决于字体是否嵌入成功。中文字体动辄几 MB很多库为了体积默认不嵌入完整字形于是导出后中文要么变方块要么变成图片格式又糊又大。插件方案更尴尬。装了插件的人电脑上有按钮换一台电脑没有就白搭你还要专门写一堆兼容逻辑。至于后端方案不是说不行而是要起服务、管任务队列、处理文件存储为了导一份报告搭这么大架子成本没必要的。浏览器打印这条路用的就是用户自己电脑上的渲染引擎所见即所得不需要任何额外运行时。所以我一般给出的第一版实现永远是这个最小骨架。它只有一段 HTML、一个按钮、两条 CSS 规则和一个 JS 函数把这份代码存成一个 .html 文件双击打开就是完整可用的 html 转 pdf 下载页面!DOCTYPE html html langzh-CN head meta charsetutf-8 title模拟项目X-月度报告/title style .no-print { display: block; } media print { .no-print { display: none !important; } .report-body { width: 100%; } } /style /head body !-- 打印按钮只在屏幕上显示 -- button classno-print onclickdownloadPdf()下载 PDF/button !-- 需要导出的内容 -- div classreport-body h1月度数据报告/h1 p这是一段支持中文、图片和表格的正文内容。/p /div script function downloadPdf() { // 打印前临时修改 document.title很多浏览器会用它作为 PDF 默认文件名 const oldTitle document.title; document.title 模拟项目X-月度报告.pdf; // 触发系统打印对话框用户选择“另存为 PDF”即完成下载 window.print(); // 打印对话框关闭后恢复原标题避免影响页面显示 document.title oldTitle; } /script /body /html这段代码的逻辑说明按钮的 onclick 绑定 downloadPdf()函数里只做三件事——保存旧标题、改 document.title、调用 window.print()。print() 是阻塞调用会停下来打开打印对话框用户选好“另存为 PDF”并点击保存后文件由浏览器直接写出。页面拿不到保存路径也不需要后端参与写文件。参数说明media print 里 .no-print 负责隐藏屏幕上才需要的按钮避免按钮印到纸上.report-body 不设固定高度让内容按自然文档流分页。这个最小版本没处理懒加载图片和表格跨页那属于第三章的内容。如果你要在这个基础上扩展只要保证一个原则屏幕样式和打印样式用同一套 DOM打印样式只做覆盖不要复制内容。2.2 打印事件的两个时机beforeprint 和 afterprint真实业务不会像最小示例那样简单页面上有图表、有滚动加载的列表、有临时需要塞进 PDF 里的信息。浏览器提供了两个非常实用的生命周期事件beforeprint 和 afterprint桌面端 Chrome、Edge、Firefox、Safari 都支持。beforeprint 在打印对话框打开前触发是最后一次“整理现场”的机会afterprint 在对话框关闭后触发用来清理现场。我一般会在 beforeprint 里做三件事把还没加载完的资源等一等、把要写进 PDF 的临时信息填进 DOM、把 document.title 改成最终文件名。在 afterprint 里则反向操作恢复 title、删掉临时节点、复位滚动位置。有一个细节要注意打印时会触发窗口 resize不要在 resize 监听里做二次渲染容易造成死循环。// 页面初始化时统一挂载打印生命周期 let originalTitle ; window.addEventListener(beforeprint, () { // 记录原标题打印结束要恢复 originalTitle document.title; // 设置 PDF 默认文件名Windows 不友好的字符会被过滤 document.title buildPdfFilename(模拟项目X-月度报告, 2025-06); // 把打印版专属的页脚信息填进 DOM屏幕上不显示 const footer document.getElementById(print-footer); if (footer) { footer.textContent new Date().toLocaleDateString(zh-CN); } }); window.addEventListener(afterprint, () { // 还原标题避免页签一直显示成文件名 document.title originalTitle; // 清空临时页脚 const footer document.getElementById(print-footer); if (footer) { footer.textContent ; } }); // 过滤 Windows 文件系统非法字符 function buildPdfFilename(prefix, dateStr) { const safe ${prefix}_${dateStr}.replace(/[\\/:*?|]/g, _); return ${safe}.pdf; }这段代码的逻辑说明把 title 的写入放在 beforeprint 里比放在按钮点击函数里更可靠。因为用户可能用 CtrlP 快捷键直接调出打印不经过你的按钮beforeprint 照样能兜住。print-footer 是一个只存在于打印样式里的节点屏幕上 display: none打印时显示用来放日期、页码这类“只在纸上出现”的信息。参数说明originalTitle 用模块级变量保存避免在 afterprint 里取不到旧值。buildPdfFilename 里的正则过滤了 Windows 文件名非法字符防止用户保存时系统弹错。dateStr 传什么格式由业务决定示例里传的是年月如果想精确到天就传 YYYY-MM-DD。这里点的目标是文件名是用户在保存对话框里最终确认的我们只能控制默认值不能强制指定。2.3 page 规则A4 尺寸、页边距与打印背景色很多导出 PDF 自带一行网址和日期看起来像水印其实是打印对话框里“页眉和页脚”开关导致的系统输出。CSS 里的 page 规则可以控制纸张大小和内容边距但没有能力直接关掉系统页眉。这里要分清两个层次page 管理的是“纸张和内容区”页眉页脚是对话框里的独立开关两者互相影响。page { /* 纸张尺寸A4 纵向 */ size: A4 portrait; /* 四边距单位 mm贴近打印习惯 */ margin: 14mm 10mm 16mm 10mm; } media print { body { /* 保留背景色和背景图否则会被浏览器默认去掉 */ -webkit-print-color-adjust: exact; print-color-adjust: exact; } .no-print { display: none !important; } .report-body { width: 100%; font-size: 12pt; line-height: 1.6; } }这段 CSS 的逻辑说明page 里的 margin 决定内容区在纸上的位置size 决定纸张类型。print-color-adjust: exact 是给浏览器的一个请求把 background-color 和 background-image 也输出到 PDF。如果不写Chrome 默认会把所有背景色当“装饰”去掉导出的表格只剩下白色底和数据文字条纹背景全消失。参数说明size 还可以写 A4 landscape 做横向签批单或者 A5 做小册子margin 不要小于 5mm打印机的物理边距普遍大于这个值设太小会被设备裁掉。font-size 在打印时建议调到 12pt 左右屏幕端的 14px 换算到纸上会偏小。如果正文包含大量表格考虑把上下边距放宽到 16mm 以上给分页位置留出喘息空间。3. 中文、图片、表格三项硬骨头为什么别人导出的 PDF 会翻车3.1 中文不乱码字体栈、字形缺失和文件编码的三层检查html 转 pdf 翻车十次里有八次是中文问题搜索引擎里搜“pdf 图片中文设置”的人特别多。先给结论浏览器打印方案下中文渲染走的是系统字体引擎只要操作系统装了中文字体就不会缺字形。真正导致乱码的通常是下面三件事里的一件页面用了远程 web font 但打印时字体还没加载完、JS 绘图方案没有把中文字体嵌入导出文件、HTML 文件本身编码声明写错了。第一层是字体栈。打印样式里把 font-family 写全优先使用系统中文字体避免每个设备因为缺字体退回不同的默认渲染。第二层是预加载。如果页面确实用了 font-face 的 icon 字体或品牌字体在 beforeprint 里用 document.fonts.ready 等字体加载完成再调 print()。第三层是编码HTML 文件开头必须写 并且文件实际保存编码要跟声明一致否则所有中文在解析阶段就已经错了再好的打印样式也救不回来。media print { body { /* 中文优先使用系统的三套中文字体按操作系统命中 */ font-family: PingFang SC, 微软雅黑, Noto Sans CJK SC, sans-serif; /* 中文标点按中文排版习惯收缩避免标点悬挂行首 */ line-break: strict; word-break: break-word; } }逻辑说明PingFang SC 对应 Mac微软雅黑对应 WindowsNoto Sans CJK SC 对应 Linux 和 Android 场景。浏览器会从左到右逐个检测当前系统有哪个用哪个都没有就回退到 sans-serif。这个顺序保证了不同设备导出 PDF 时的中文字形稳定一致。参数说明line-break: strict 是中文排版规则句号、逗号、引号会按中文习惯处理不会孤零零出现在行首。word-break: break-word 防止长英文或 URL 把表格列撑破。如果读者反馈“在 Windows 上导出是宋体在 Mac 上是苹方”那不是错误是字体栈依次命中导致的正常差异PDF 打开效果不会缺字。3.2 图片不丢失懒加载回填、完整地址与 background-image 的坑图片在 PDF 里消失多半是三种原因图片地址是相对路径或懒加载未触发、background-image 被打印样式禁用、图片还没加载完打印就开始了。现在主流网站都用懒加载img 标签的真实地址放在>// 打印前统一处理图片回填懒加载地址并等待解码 async function prepareImages(root document) { // 1. 回填所有懒加载图片的真实地址 root.querySelectorAll(img[data-src]).forEach((img) { if (img.dataset.src !img.src) { img.src img.dataset.src; } }); // 2. 过滤出需要等待的网络图片 const images Array.from(root.querySelectorAll(img)) .filter((img) img.src img.src.startsWith(http)); // 3. 并发等待解码单张失败不阻断全局 await Promise.all( images.map((img) img.decode().catch(() console.warn(image decode failed:, img.src)) ) ); }逻辑说明这个函数解决“图片显示一半”和“图片位置空白”两个典型故障。第一步处理懒加载组件留下的>media print { table { /* 合并边框避免分页处出现双重边框 */ border-collapse: collapse; width: 100%; page-break-inside: auto; } thead { /* 表头在每一页顶部都重复打印 */ display: table-header-group; } tr { /* 禁止一行被拆到两页 */ break-inside: avoid; page-break-inside: avoid; } td, th { border: 1px solid #333; padding: 6px 8px; vertical-align: top; word-break: break-word; } }逻辑说明display: table-header-group 是这一节里最重要的属性它告诉打印引擎把 thead 当作“跨页重复的表头组”每页顶部自动重渲一次。tr 的 break-inside: avoid 让一行整体移动到下一页而不是在页边处劈开。border-collapse 合并相邻边框避免分页线处出现加粗的边界。参数说明page-break-inside: avoid 绝对不能加在 table 上。新手很容易顺手把避断规则加到整表结果是打印引擎把整张表当做一个整体块第一页留下一大段空白表直接挪到第二页。另外如果表格带 rowspan 或 colspan部分浏览器分页会变得不稳定这时最稳的兜底方案是后端或前端按每页 2530 行手工切片每个分页包裹一个独立的 table 和 thead而不是完全依赖打印引擎推算。4. 把“下载”做实PDF 文件名、打印对话框参数与多页控制4.1 文件名怎么控制document.title 是唯一可靠的抓手“文件下载”这个词在 html 转 pdf 场景里经常被误解。浏览器打印出来的 PDF保存对话框里的默认文件名取自页面改 title 就能控制默认文件名。但打印对话框是浏览器原生窗口JavaScript 拿不到用户选定的保存路径也不知道用户是点了保存还是取消。这是浏览器的安全设计页面脚本不能绕过系统对话框直接写文件到磁盘。/p p所以“下载”落地是两件事确保默认文件名正确、确保用户操作路径清晰。文件名控制在代码层面就一句话打印前改 document.title。要注意 Windows 文件系统不允许文件名包含 : * ? | 这些字符拼文件名时先过滤一遍。有人试过给 window.print() 传参指定文件名那是无效的print() 不接收文件名参数。/p precode classlanguage-javascript// 在调用打印前设置默认文件名 function downloadPdf() { const base 模拟项目X-月度报告; const safe ${base}_2025-06.replace(/[\\/:*?|]/g, _); const oldTitle document.title; document.title ${safe}.pdf; window.print(); document.title oldTitle; } /code/pre p逻辑说明replace 用正则把 Windows 非法字符统一替换成下划线避免保存时系统弹错误。document.title 在 print() 调用前写入浏览器打开打印对话框时已经读取了新标题。afterprint 之后再恢复原标题防止页签长时间显示成带 .pdf 后缀的文件名。/p p参数说明如果你在 beforeprint 里已经处理过 title那就不需要在按钮函数里重复设置两边选一处维护即可。单页应用里 title 通常由路由管理直接改 document.title 可能被路由监听覆盖这时候要走框架提供的 title 管理接口打印完成再恢复。相比 JS 绘图库能精确控制导出文件名浏览器方案只能控制默认值最终文件名由用户在保存对话框里确认这一点要在需求沟通时讲清楚。/p h34.2 打印对话框里的选项与 page 参数对照/h3 p打印对话框里有几个选项直接决定 PDF 成品的长相很多人搞不清它们和 CSS 的关系。我列一张对照表照着查就行/p table thead tr th对话框选项/th th作用/th th是否能被 CSS 控制/th th处理方式/th /tr /thead tbody tr td目标打印机/td td选“另存为 PDF”或系统 PDF 打印机/td td不能/td td引导用户在选择列表里选 PDF 输出/td /tr tr td页眉和页脚/td td顶部网址、标题、日期、页码/td td不能/td td提示用户取消勾选/td /tr tr td背景图形/td td是否输出背景色和背景图/td td可以请求最终由开关决定/td tdCSS 加 print-color-adjust: exact/td /tr tr td边距/td td内容与纸边的距离/td td可以page margin/td td设 1016mm别小于 5mm/td /tr tr td纸张大小/td tdA4 / A5 / 横向/td td可以page size/td td按业务选 portrait 或 landscape/td /tr /tbody /table p页眉和页脚是“无需插件”方案里唯一需要人工配合的系统级开关。我的习惯是在页面按钮旁边放一行灰色小字提示请在弹出的打印窗口中选择“另存为 PDF”并取消“页眉和页脚”。这行提示通过 .print-tip 类实现屏幕上显示、打印时隐藏不影响成品。/p precode classlanguage-css.print-tip { color: #666; font-size: 12px; } media print { .print-tip { display: none; } } /code/pre p逻辑说明.print-tip 在屏幕上给用户操作指引进入打印流时隐藏。因为系统页眉页脚无法用 CSS 消除唯一的办法是让用户手动取消勾选提示文案是成本最低的兜底。如果你面对的导出场景是内部系统、操作者固定可以把提示直接写进页面顶部比每次弹对话框再解释效率高。/p p参数说明排版时上边距设 1416mm 能避免用户忘记取消页眉时系统网址压住正文首行确认用户一定会取消页眉后可以缩回 10mm。不同浏览器对页眉页脚的默认开关不一致Chrome 默认关背景图形Edge 默认开背景图形所以同一个页面在两个浏览器导出的效果会有视觉差异。让后台接收端不要只盯着一张截图渲验证至少要跑两个主流浏览器各导一份。/p h34.3 多页内容与章节分页break-before 控制每一章的起始页/h3 p长报告导出 PDF最影响观感的是章节之间没有分页上一章结尾和下一章开头挤在同一页。解决方式是给每个章节容器加 break-before: page旧版本的 page-break-before: always 也一起写上兼容老内核。这个规则对表格、标题、图片都生效是排版层面的核心控制。/p p另一个高频需求是“章节标题不要单独落在页尾”。一本书也好、一份报告也好如果某页底部只有一行“3.2 供应链风险”翻页才是正文看起来非常业余。对应规则是给 h2、h3 加 break-inside: avoid并给标题和后续内容建立连续关系。/p precode classlanguage-cssmedia print { /* 新的一章从新页开始 */ .report-chapter { break-before: page; page-break-before: always; } /* 标题不要孤悬在页尾 */ .report-chapter h2 { break-after: avoid; } h2, h3 { break-inside: avoid; } } /code/pre p逻辑说明break-before: page 让每个章节块强制从新页顶部开始break-after: avoid 让标题和它后面的第一段正文尽量留在同一页。这样标题不会出现在一页的最底部正文在下一页。这两个属性合起来报告的分页观感会明显改善。/p p参数说明不要对每个表格都加分页短表格前后会各留一整页空白纸张浪费且观感差。分页规则和 page margin 联动边距改一次所有断点位置都会重排比如把上下边距从 14mm 改成 20mm原本一页能装下的表格可能被推到第二页。改完边距必须重新跑一遍完整打印预览检查断点。/p h25. 常见问题与排查乱码、图片消失、表格截断的五个深坑/h2 h35.1 打印对话框里找不到 PDF 打印机/h3 p现象点击打印按钮后弹出系统打印对话框但目标打印机列表里找不到“另存为 PDF”选项只有物理打印机或者连 PDF 输出都没有。/p p原因浏览器和操作系统版本差异。Windows 上打印到 PDF 由系统层的 PDF 打印机提供如果系统组件缺失浏览器对话框里就不会出现macOS 的“存储为 PDF”藏在左下角的下拉菜单里手机端浏览器则是先走“打印”再选“保存为 PDF”路径藏在子菜单里。用户在不同设备上操作路径完全不一样很容易觉得功能失效。/p p解决Windows 在系统设置的可选功能里启用系统 PDF 打印机macOS 在打印对话框左下角下拉菜单里选“存储为 PDF”手机端在打印选项里找“保存为 PDF”或“存储为 PDF”。交付方案时把这三条路径写进说明比用户自己摸索省时间。这个问题不是代码问题是运行环境的系统能力问题。/p h35.2 中文变成方块或问号标题和正文字形不一致/h3 p现象导出 PDF 里中文变成豆腐块状的空白方块或者标题是一种字体、正文是另一种字体风格明显割裂。/p p原因两种典型情况。第一种页面用 font-face 加载远程中文字体打印时字体文件没有加载完浏览器退回系统默认字体但某些字形渲染不正常。第二种项目用了 JS 绘图库把 DOM 画到 canvas 上字体没有完整嵌入导出文件PDF 阅读器打开了却没有对应字形文件。纯打印路线遇到这类问题绝大多数是字体栈没写全。/p p解决打印样式里写完整的中文字体栈并把远程字体的 font-display 设为 block在 beforeprint 里用 document.fonts.ready 等字体就绪。排错顺序先看屏幕显示是否正常屏幕正常但 PDF 乱码是打印时字体加载时机问题屏幕就不正常是字体栈或文件编码问题。这两个方向排查路径完全不同别混着查。/p h35.3 图片在屏幕正常打印出来却是空白或缩成一团/h3 p现象页面上图片清晰可见导出 PDF 后图片位置空白或者只剩一个很小的占位框。/p p原因三种情况要分开排查。第一种懒加载图片的 src 还是占位图真实地址放在>