jsPDF导出PDF中文乱码?三大方案助你搞定表格

发布时间:2026/10/17 9:57:18
jsPDF导出PDF中文乱码?三大方案助你搞定表格 最近在做某个管理系统的时候又碰到了jsPDF导出PDF中文乱码这个老朋友。说起来这个坑我前前后后踩了不止一次每次换项目都能遇到而且每次的环境还都不一样有的项目里是表格数据全是中文导出后PDF直接变成乱码方块有的项目里是部分中文正常、部分还是乱码还有的是本地开发好好的部署到服务器上就出问题。这篇文章就把我这几轮实操沉淀下来的经验完整梳理一遍围绕jsPDF的字体处理、数据表格导出、中文显示这几个核心点给出可直接照抄的解决方案也帮新手朋友避开我踩过的那些坑。先说下适用范围如果你的项目是纯前端导出PDF用的技术栈里有jsPDF并且表格数据涉及中文那么这篇内容就是为你准备的。无论你是用Vue还是React无论你用的是最原始的jsPDF API还是搭配了jspdf-autotable插件下面这套方案都能覆盖到。1. 乱码问题的根因与解决思路1.1 jsPDF为什么会显示中文乱码要先说清楚一个本质问题jsPDF本身不是一个“完全开箱即用”的中文PDF生成工具。它默认内置的字体只有标准拉丁字符集比如Helvetica、Times、Courier这类这些字体文件内部只包含英文字母、数字、常用符号的轮廓数据压根没有中文字形。PDF的逻辑是文档内容里每渲染一个字符都需要在字体资源里找到对应的字形轮廓然后画到页面上。如果字体文件里没有这个字形渲染引擎就只能用一个占位符替代表现出来就是方框、问号、空白或者一段乱码。所以jsPDF在纯默认配置下遇到“中文”这两个字自然是无能为力。很多人第一步尝试是把中文内容直接传给pdf.text()方法然后发现出来的是乱码这时候第一反应是“是不是编码问题”于是去试各种各样的编码转换折腾半天根本没用。问题不在字符串的编码而在字体资源缺失。这个认知不纠正过来后面所有的尝试都会跑偏。1.2 乱码的几种典型表现我实际工作中遇到过的乱码形态大概有这么几种你可以对照一下自己的情况第一种是整段中文全变成小方块英文和数字正常。这是最典型的字体缺失信号说明文本里的中文字符在字体资源里完全找不到对应字形。第二种是中文字符变成了类似“中文”这样带重音的字符序列。这种往往是因为文本在进入PDF之前就被错误编码了比如字符串从某个接口拿回来时已经是乱码或者UTF-8数据被按Latin-1转了一遍。这种情况要检查数据链路不光是字体问题。第三种是中文显示空白连方块都没有。这个少见一些但有时候会出现在某些PDF阅读器对嵌入字体解析不完整的情况下字符宽度算出来了但字形没渲染出来。第四种比较隐蔽就是本地测试一切正常部署到线上就乱码。这种情况十有八九是字体文件加载策略的问题比如本地开发时静态资源服务器能直接访问到字体文件而生产环境字体文件被CDN缓存或者跨域限制拦截了。1.3 解决思路的整体选型搞清楚根因之后解决路线就很清晰了核心就是想办法给PDF塞进一套中文字体并且让jsPDF在渲染时明确使用这套字体。根据生成PDF的方式不同主流方案分这么几类一类是“所见即所得”的截图式方案先用html2canvas把你页面上的表格DOM节点整个截图成一张图片再把图片嵌入PDF。这种方式的好处是不用管字体问题页面上能显示出来就能导出到PDF里但是生成的是位图文字不可选中、不可搜索而且放大后会有发虚的问题。另一类是“矢量文本”方案把中文字体文件通过base64编码注册进jsPDF然后直接用pdf.text()逐行绘制文本内容。这种方式生成的PDF是真正的矢量文字文字清晰可选中文件体积也小但需要自己处理字体注册而且字体文件动辄十几MB转base64后字符串很长内存占用不小。还有一类是表格专用方案用jspdf-autotable这个插件来解析表格数据它在绘制单元格文本的时候允许指定自定义字体只需要把中文字体注册到jsPDF实例上然后在表格样式里把font字段指向这个字体名称即可。这三类方案适应场景各不相同数据量小、要求还原度高的场景我建议用截图式数据量大、要求文字可搜索可复制并且需要控制文件体积的场景矢量文本方案是首选而纯表格结构的场景autotable方案是兼顾开发效率和输出质量的最优选。2. 方案一html2canvas截图直出PDF最快但要注意还原度2.1 基本原理和适用场景html2canvas方案的核心思路是绕过字体内嵌的复杂环节直接把DOM渲染成Canvas位图然后再以PNG或者JPEG的格式贴到jsPDF创建的页面上。这样做的好处是所有页面上已经渲染好的效果都会被“原样带走”不需要你去操心CSS样式、字体、颜色、边框这些细节。比较适合的使用场景是表格结构不复杂数据量不大比如一页到两页能展示完对PDF的文字可编辑性没有要求只求快速实现。例如给运营导出数据看板截图、给财务导出简单的月度汇总表这类需求用截图式是最节省开发时间的。但有两个点必须提前知道第一html2canvas并不是对所有CSS属性都完美支持它内部是模拟浏览器渲染所以像box-shadow、某些渐变、部分伪元素可能渲染得有偏差第二截图式生成的是图片如果你导出的表格有很多页图片会非常大PDF页面尺寸有限文字会被压缩得很小这一点需要在代码里做好缩放和分页处理。2.2 完整实现步骤第一步安装依赖。html2canvas和jspdf都要装命令是npm install html2canvas jspdf——版本我建议直接用最新的稳定版遇到问题也好查文档。第二步在组件里引入两个库代码如下。import html2canvas from html2canvas; import { jsPDF } from jspdf;第三步编写导出的核心方法。首先要确保你要截图的DOM元素确实渲染完成最好在调用导出逻辑前强制刷新一下布局避免截图时机过早导致内容缺失。然后通过canvas.toDataURL()拿到图片编码再计算出图片在PDF中的尺寸比例。async function exportTableToPdf(domId, fileName 表格导出) { const dom document.getElementById(domId); if (!dom) return; // 等待渲染稳定 await new Promise((resolve) { window.requestAnimationFrame(() resolve()); }); const canvas await html2canvas(dom, { scale: 2, // 提高清晰度2倍缩放效果较好 useCORS: true, backgroundColor: #ffffff, logging: false }); const imgData canvas.toDataURL(image/png); const pdf new jsPDF({ orientation: landscape, unit: mm, format: a4 }); const pageWidth pdf.internal.pageSize.getWidth(); const pageHeight pdf.internal.pageSize.getHeight(); const imgWidth pageWidth; const imgHeight (canvas.height * imgWidth) / canvas.width; let positionY 0; let leftHeight imgHeight; // 如果图片高度超过一页可容纳的高度就分页处理 while (leftHeight 0) { pdf.addImage(imgData, PNG, 0, positionY, imgWidth, imgHeight); leftHeight - pageHeight; if (leftHeight 0) { positionY - pageHeight; pdf.addPage(); } } pdf.save(${fileName}.pdf); }第四步你只需要在页面按钮的点击事件里调用exportTableToPdf(exportTable)就可以了。这里有几个细节要提醒一下scale参数建议不要低于2否则高清屏上截出来的图会很模糊orientation最好根据你的表格长宽比来定宽表格用landscape长表格用portrait分页逻辑里positionY用负数累加是因为addImage的坐标是相对于页面左上角多页时图片要往上移动。2.3 这个方案的局限和性能问题截图式方案实现简单但也别把它想得太美好。我曾在一个数据量比较大的场景里试用过表格有三四页内容直接整个DOM截下来结果canvas生成的dataURL字符串非常长内存占用直接冲高页面明显卡顿导出后的PDF文件也有好几MB因为里面全是位图数据。所以如果用这个方案我建议先把表格分页只截当前页面显示部分或者把数据表和导出按钮所在的容器拆开避免把无关的按钮、标题、侧边栏都截进去。另一个实践是如果表格内容特别宽可以考虑让html2canvas的scale参数控制在1至2之间否则文件过大用户下载体验也差。还有一点容易忽略html2canvas在渲染跨域图片时需要服务端配合开启CORS否则图片区域会导出为空白。如果是内部系统没有跨域图片就没事一旦表格里有头像、商品图这类远程资源就要提前确认图片服务器是否允许跨域访问。3. 方案二中文字体注册矢量文本绘制文件小且文字可选中3.1 中文字体文件的获取与基础转换矢量文本方案的精髓在于把一套完整的中文字体文件嵌入到PDF文档里。PDF规范本身支持内嵌字体jsPDF也提供了相关的API只是没有默认给中文预备好字体。所以我们需要手动完成三件事获取字体文件、转换成base64编码、注册到jsPDF实例。获取字体文件的时候要注意版权问题。我一般推荐用开源的中文字体比如思源黑体、思源宋体、Noto Sans SC这类也可以直接用系统中已有的字体文件比如Windows下的simhei.ttf黑体、simsun.ttc宋体。不过ttc格式是老版本字体合集某些旧版jsPDF对ttc的解析可能会有兼容性问题我建议优先选ttf格式。字体文件本身比较大的问题也需要考虑思源黑体简体中文版TTF通常在8MB到15MB之间这对一个前端资源来说不算小。如果不想让打包体积爆炸可以把字体文件放到静态资源目录里运行时动态请求而不是直接import到代码里。动态请求后转base64的代码如下。async function fontToBase64(fontUrl) { const response await fetch(fontUrl); const blob await response.blob(); return new Promise((resolve) { const reader new FileReader(); reader.onload () resolve(reader.result); reader.readAsDataURL(blob); }); }这个函数返回的是DataURL格式类似“data:font/ttf;base64,xxxx”的样子后面注册字体时需要把前缀去掉只保留base64部分。3.2 注册字体并绘制中文文本jsPDF注册字体的API主要有两个addFileToVFS用于把字体文件内容挂到虚拟文件系统上addFont用于声明字体名称和样式。然后通过setFont切换到自定义字体。具体代码如下。import { jsPDF } from jspdf; async function createPdfWithChineseText() { // 1. 加载字体文件的base64内容 const dataUrl await fontToBase64(/fonts/SimHei.ttf); const base64 dataUrl.split(,)[1]; // 2. 创建PDF实例 const pdf new jsPDF({ unit: mm, format: a4 }); // 3. 注册字体到虚拟文件系统 pdf.addFileToVFS(SimHei.ttf, base64); pdf.addFont(SimHei.ttf, SimHei, normal); // 4. 设置当前字体 pdf.setFont(SimHei); // 5. 绘制文本 pdf.text(这是一段中文测试文字, 20, 30); pdf.text(第二行表格数据导出示例, 20, 40); pdf.save(中文文本.pdf); }这段代码看起来很简单实际使用时候有几个细节非常关键。第一个细节是fontName一旦注册错了后续setFont和text都会静默失败或者直接报错“undefined is not an object”。addFont的第二个参数就是你指定的字体标识setFont里传的必须和它完全一致大小写、空格都不能差。第二个细节是字体样式。如果你在addFont里注册的样式是“normal”那么绘制时想要加粗文字是不能靠setFont(style bold)实现的因为PDF里没注册对应的加粗字形资源。解决方式是多注册一份同一字体文件但样式名不同的资源或者直接用原字体的加粗版本比如SimHei本身笔画就粗一般不需要额外加粗。第三个细节是文字位置参数。pdf.text(text, x, y)中x和y是PDF坐标系的位置单位是毫米。很多人会把y当作文本顶部的坐标实际上jsPDF的text默认将y坐标当成文本基线的位置。如果你想让文本从某个位置开始向下排列需要考虑行高比如每行加5mm到6mm的间距。3.3 表格数据手动绘制示例在矢量文本方案下表格需要你自己手动绘制网格线和文本相当于把表格渲染的逻辑从HTML搬到了PDF坐标系里。虽然繁琐但可控性最强。我这里给你一个简单的示例假设有三列数据姓名、部门、工号。const tableData [ { name: 张伟, dept: 研发部, empId: A001 }, { name: 李娜, dept: 市场部, empId: A002 }, { name: 王强, dept: 财务部, empId: A003 } ]; function drawTable(pdf, startX, startY, colWidths, headers, rows) { const cellHeight 8; // 绘制表头 let cursorX startX; const headerY startY; pdf.setFont(SimHei); pdf.setFontSize(10); pdf.setFillColor(230, 230, 230); headers.forEach((header, idx) { pdf.rect(cursorX, headerY, colWidths[idx], cellHeight, FD); pdf.text(header, cursorX 2, headerY 5.5); cursorX colWidths[idx]; }); // 绘制数据行 let cursorY headerY cellHeight; rows.forEach((row, rowIdx) { cursorX startX; if (rowIdx % 2 0) { pdf.setFillColor(245, 245, 245); pdf.rect(startX, cursorY, colWidths.reduce((a, b) a b, 0), cellHeight, F); } const values [row.name, row.dept, row.empId]; values.forEach((val, idx) { pdf.text(val, cursorX 2, cursorY 5.5); cursorX colWidths[idx]; }); cursorY cellHeight; }); } // 使用示例 const pdf new jsPDF({ unit: mm, format: a4 }); // 这里假设已经注册了SimHei字体 pdf.setFont(SimHei); drawTable( pdf, 20, 30, [50, 50, 40], [姓名, 部门, 工号], tableData ); pdf.save(手绘表格.pdf);手动绘制表格的代码量不小但好处也直接你能精确控制每个单元格的坐标、字体大小、行高、填充色完全可以按自己的视觉需求调校出一套符合公司风格的模板。只是如果表格要跨页一定要在绘制数据行的时候判断cursorY是否超过当前页的可打印高度超出就调用pdf.addPage()新建一页并把光标重置回页面顶部。3.4 字体文件大小对性能的影响矢量方案生成的PDF文件通常只有几十KB到几百KB远比截图式的几MB要友好这是因为PDF里内嵌字体只保存一份字形数据所有文本都是对这些字形的引用体积和文本长度没有特别大的关系。不过加载字体文件本身是需要时间的。字体文件放本地静态目录还好如果放在CDN上第一次请求可能要几秒才能完成体验上会有一个明显的等待。我的建议是在系统启动时或者用户第一次进入导出页时提前把字体文件预加载好并缓存到内存里这样可以避免每次导出都重复请求同一个文件。一个值得关注的坑是Vue或React项目如果把字体文件直接import打包器可能会生成一个独立的字体文件加上一串很长很长的base64字符串打包体积会上升但如果使用了按需加载和懒加载配置影响不是太大。个人更推荐运行时fetch的方式把字体文件路径放到public目录下或者用远程CDN地址这样构建过程没有额外负担也不会影响首屏加载速度。4. 方案三jspdf-autotable插件集成表格导出效率最高4.1 插件能帮你解决哪些问题如果你导出的内容主要是结构化表格手动绘制每一行每一列确实有点繁琐这时候jspdf-autotable就派上用场了。这个插件专门负责把JavaScript数组或者HTML表格数据渲染成PDF表格自动处理表头、列宽、行高、分页、页脚等逻辑你只需要传入数据和一些样式选项。它的一个核心特性是允许通过styles.font字段指定一个已注册的字体名称所以我们只需要把中文字体注册到jsPDF实例上然后在autoTable的配置里指向该字体单元格内的中文就能正常显示。在版本选择上注意一下老版本的autotable是给jsPDF实例挂一个autoTable方法新版本3.x则直接导出一个默认函数写法上有所不同。我建议使用新版本调用方式统一文档也更清楚。4.2 引入依赖与初始化配置安装依赖的命令是npm install jspdf-autotable。然后在代码里引入注意这是函数式引入不是把整个库挂到原型上。import { jsPDF } from jspdf; import autoTable from jspdf-autotable;接着我们就可以写一个导出表格的基本方法了。字体注册的流程和方案二完全一样先加载字体转base64再addFileToVFS、addFont然后调用autoTable时在styles里配置font字段。async function exportTableWithAutoTable(headers, rows, fileName 报表) { // 1. 加载字体 const dataUrl await fontToBase64(/fonts/SimHei.ttf); const base64 dataUrl.split(,)[1]; // 2. 创建PDF并注册字体 const doc new jsPDF({ orientation: landscape, unit: mm, format: a4 }); doc.addFileToVFS(SimHei.ttf, base64); doc.addFont(SimHei.ttf, SimHei, normal); doc.setFont(SimHei); // 3. 使用autoTable绘制表格 autoTable(doc, { head: [headers], body: rows, startY: 20, styles: { font: SimHei, fontSize: 9, cellPadding: 3, overflow: linebreak }, headStyles: { fillColor: [85, 85, 85], textColor: 255, fontStyle: normal }, alternateRowStyles: { fillColor: [245, 245, 245] } }); // 4. 保存 doc.save(${fileName}.pdf); }这里的核心逻辑只有两步注册字体然后把font选项指向SimHei。如果漏掉styles.font配置表格里的中文会再次变成方块乱码这是新手最容易犯的错误。4.3 表头、边框、分页的精细调优默认的autoTable样式其实比较素如果你希望表格更符合企业导出规范可以通过headStyles、bodyStyles、alternateRowStyles、columnStyles等配置调整。实际项目中我常用的一组配置是这样的表头背景色用深灰或品牌色fillColor: [70, 91, 158]表头字体颜色用白色textColor: 255表头字号稍大一些fontSize: 10正文行高控制在7mm左右cellPadding取2到3长文本自动换行overflow: linebreak行间用交替色增强可读性alternateRowStyles分页方面autoTable内部已经实现了自动分页表格超过一页时会自动截断并创建新页同时默认不会自动重复表头。如果你希望第二页的表头也显示出来需要设置didDrawPage回调或者使用repeatHeaderyes选项老版本用法不同。新版本中也有一个字段来控制我一般直接利用它的默认逻辑然后在每页手动添加页脚标题这样看起来更专业。autoTable(doc, { // 其他配置... didDrawPage: (data) { const pageCount doc.internal.getNumberOfPages(); doc.setFontSize(8); doc.text(第 ${data.pageNumber} 页 / 共 ${pageCount} 页, 200, 290, { align: right }); } });4.4 多页大数据量表格的渲染性能当表格行数非常多几千行数据一次性塞给autoTable时性能瓶颈主要出现在两个地方一是在数据传递之前你如何组织数组二是字体文件base64字符串会占用大量内存。我的经验是尽量让后端做数据聚合前端只拿当前需要展示或者导出范围内的数据比如导出的数据量限制在2000行以内如果超过这个量就分批生成多个PDF或者做二次确认。另外fontToBase64返回的字符串不要存在全局变量里用完即释放避免长时间占内存导致页面卡顿。还有一个很实用的小技巧对包含大量文本的单元格提前做一个字符串截断比如导入导出的备注列限制了最多200个字符超出部分用省略号表示这样PDF渲染时文本布局的计算量会小很多分页也变得干净利落。5. 常见问题与排查技巧实录5.1 字体注册了但中文还是要乱码这个问题的概率最高。多数原因是addFont和setFont里的字体名称不一致或者是字体注册发生在创建PDF实例之前导致addFileToVFS写入的虚拟文件系统没被当前实例识别。我排查这类问题时第一件事就是打印出pdf.getFontList()看看返回的字体列表里是否包含你注册的字体名称。const fontList pdf.getFontList(); console.log(fontList);如果列表里没有你注册的名称那说明addFont没有生效需要检查字体文件base64是否正确、是否是TTC导致解析失败、字体标识是否拼写一致。如果列表里有了但在渲染时依然乱码就要检查是不是在某些回调里又重新创建了一个jsPDF实例比如在autoTable的didDrawPage回调里又new了一个doc这时候注册的字体当然不在新实例里。5.2 页面显示正常导出后中文字体大小不一致还有一种情况是页面表格里的中文用的是系统和浏览器字体导出到PDF里虽然不乱码但字体样式和页面上明显不一样比如页面上是雅黑PDF里是宋体看起来就很突兀。这不是bug而是字体未匹配导致的。要解决也很简单让页面和PDF使用同一套字体。你可以在CSS里对目标表格设置同样的字体族比如font-family: SimHei然后PDF也注册同名字体。这样页面和导出文件的视觉风格基本一致。不过要真做到像素级一致最可靠的方式还是html2canvas那条路。5.3 字体文件跨域加载失败把字体文件放在CDN上时如果CDN返回的CORS头不完整浏览器的fetch会直接报错。调试手段是打开F12看Network面板里字体请求的状态如果出现CORS错误就需要让运维在CDN配置中添加Access-Control-Allow-Origin头或者改用同源部署。还要注意一个兼容性细节老版本的浏览器对客户端fetch一个大文件并转成DataURL的兼容性不太好如果你们的系统还在支持较老的WebView内核建议用XMLHttpRequest发送请求拿到responseText之后再手动拼接base64前缀这样兼容面更大。5.4 导出内容超出页面边界这个问题常见于表格列数特别多或者单列文本特别长的场景。PDF页面宽度有限如果你的表格有10列每列就算字体9号也可能挤满A4横向页面。遇到这种场景要么把页面方向改成landscape要么调整列宽比例要么对每列做宽度限制并开启自动换行。autoTable里有一个方便的做法在columnStyles里指定百分比宽度或者固定宽度比如第3列特别长可以设置columnStyles: { 2: { cellWidth: 60 } }其他列自动分配剩余宽度。这样即使某些单元格内容再长也不会突破页面边界。手动绘制方案就麻烦一些需要通过文字宽度计算函数来预估字符串是否超长再手动换行或者截断。5.5 表格跨页时表头丢失分页后表头不显示是导出报表时很容易被挑剔的需求点。autoTable新版本默认跨页时会尽可能保留表头的设置如果你的版本没有自动重复表头建议升级到3.x或者用didDrawPage回调手动绘制表头。手动绘制表格的方案里这点只能自己做在你的数据行渲染逻辑中加入页高度判断一旦超过阈值就addPage并把表头重新画一遍。5.6 导出文件名带中文时下载失败或者乱码文件名本身也可能造成问题。pdf.save(中文文件名.pdf)在一些浏览器或者移动端环境下会出现文件名乱码这个是浏览器对中文URL编码处理不一致导致的。我自己常用的规避方式是把文件名统一转成拼音或者英文字符比如“Export_20250118.pdf”或者保存时用Blob对象把UTF-8编码的文件名放进response header中再通过URL.createObjectURL触发下载。但大多数人导出PDF是本地点击按钮保存直接在save里传英文文件名最省心。6. 我的实操体会这几个方案我在真实项目里都完整落地过综合感受是纯表格报表导出autotable方案综合性价比最高字体问题一劳永逸地解决样式调完一次就能复用需要保留页面原貌、图片表格混合内容的场景html2canvas方案最省心而对导出的PDF有文字搜索、复制、文件体积敏感的合规要求矢量方案是唯一选择。另外有两个经验之谈。第一无论选择哪个方案中文字体文件一定要在项目里统一管理我一般会单独建一个fonts目录把常用的黑体、宋体文件放好同时建一个字体加载的工具模块封装fontToBase64和字体注册逻辑这样所有导出功能都走同一套字体不会出现A模块正常、B模块乱码的分裂情况。第二导出PDF这类功能虽然不起眼但往往是用户高频使用的功能出一次乱码问题就很影响体验所以封装好之后再遇到类似需求直接复用比每次重新踩坑要省太多时间了。如果你现在的项目也是用jsPDF做表格导出建议先按方案三把autotable跑通给你的项目把中文字体注册好再拿一个真实的表格数据测试一遍之后遇到样式和分页的细节问题再逐步调优这样上手最快也不会产生挫败感。

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询