pdfmake中文乱码解决:字体子集化与vfs_fonts配置详解
2026/9/7 8:58:07 网站建设 项目流程

简介:pdfmake 是浏览器端生成 PDF 的流行开源库,但默认字体并不包含中文字形,导致导出文档经常出现方框乱码。这款资源包面向前端开发者,提供了一套免后端、纯浏览器运行的完整示例:下载解压后,直接打开 test.html,点击页面上的下载按钮,即可得到一份中文无乱码的 PDF 文件,直观看到方正字体带来的效果。资源包共 4 个文件,核心包括 1 个 HTML 演示页、1 个主库 JavaScript 脚本和 1 个方正字体 JavaScript 脚本,另有一个系统自动生成的 .DS_Store 元数据文件,不影响使用;整个压缩包仅 2.36MB,非常轻量。目前已有 1307 人学习/下载,适合刚接触 pdfmake、需要在项目中输出报表、合同、发票等文档的开发者参考。通过查看演示页中的调用方式与字体注册写法,可以快速理解中文 PDF 的配置思路,并把字体文件和初始化逻辑直接移植到自己的前端工程中,免去反复调试乱码的烦恼。

1. 项目背景:为什么pdfmake导出中文会乱码

先说结论:pddfmake本身不是不支持中文,而是它默认加载的字体文件里没有中文字形。这个坑我踩过好几次,最开始用pdfmake做前端导出PDF的时候,英文数字一切正常,一到中文就变成一排排的方块或者问号,网上搜了一圈,翻到的大多是把字体转成vfs_fonts.js再引入、然后改defaultStyle.font的步骤,但里面的细节——比如字体子集化怎么处理、字体文件为什么那么大、动态文本怎么传参——没人讲清楚。

这个需求典型出现在什么场景呢?管理后台的报表导出、订单明细下载、简历生成、发票打印,只要你的用户群体是中文环境,PDF导出就绕不开中文乱码这件事。pdfmake的优势在纯前端,不用后端渲染模板,也不用装什么转换服务,一个npm包就够,所以这个方案在很多中小型项目里使用频率很高。

这篇文章我就把自己的完整实现过程、踩过的坑、以及最终能跑通的配置全部写出来。不管你是第一次接这种需求,还是已经改了好几轮乱码但没根治,照着下面的流程走一遍,基本上能一次解决。

2. 乱码根源解析

2.1 字体缺失是唯一原因

pdfmake底层用的是pdfkit(一个老牌的PDF生成库),PDF文件里的文字并不是把“字”存进去,而是存“字符编码 + 字形引用”。你看PDF里能显示中文,是因为PDF文件里嵌入了一种叫CIDFont的字体资源,这种字体把这些字符映射到了具体的字形上。pdfmake默认内置的Roboto字体,只覆盖拉丁字符和部分符号,压根没有汉字映射表,所以遇到中文时,要么显示不出字形,要么直接乱码。

对比一下:你用Word导出PDF,Microsoft Word会把宋体或者微软雅黑一起嵌入进去,所以任何电脑打开都正常;pdfmake不可能默认带中文字体,因为中文字体动辄十几MB,没哪个库敢默认打包进去,这是它不做中文字体的根本原因。

2.2 各种“乱码”形态对应的不同问题

我在实际使用中见过三种“乱码”表现,成因不完全一样:

  • 方块字(□□□□):最常见,字体文件没嵌入中文字形,PDF viewer显示不了。
  • 中文挤成一团但英文正常:Roboto对中文的fallback处理出错,通常是因为字体注册顺序有问题,导致最佳匹配失败。
  • 导出后文件名乱码但内容正常:这个跟pdfmake字体无关,是浏览器download属性编码问题,后面我会专门说。

第一种走“注册中文字体”这条路线就能解决,第二种多半是你在生成docDefinition的时候,手动给每个text都标了font属性,反而干扰了全局字体设置,第三种别怪pdfmake,接口层处理一下URL编码就行。

3. 字体选型与准备

3.1 中文字体怎么选

中文PDF需要嵌入中文字体,但中文字体不能随便选。得综合考虑体积、版权、字形完整度三件事。

我这边实际测试下来,适合pdfmake用的中文开源字体主要有三款:

  • 思源黑体(Source Han Sans):Google和Adobe联合出品,字形规范,开源免费,对应grep源码包里的SourceHanSansSC-Normal.otf,体积较大,完整版可达16MB以上。
  • 思源宋体(Source Han Serif):适合公文、论文风格,同样体积很大。
  • 文泉驿微米黑:开源中文字体,体积相对小一点,但字形老一点,不过日常使用完全够。

有人会问,为什么不用微软雅黑或者宋体?这两个字体虽然系统里有,但Windows系统字体版权是微软的,你把它打包进项目再分发给用户,有法律风险;而且微软雅黑的字重设计是针对显示器的,不是针对印刷和PDF的,在打印场景下不如思源黑体。

再补充一点,pdfmake支持TTF和OTF字体,我推荐用TTF,因为加载速度快一点。思源黑体的OTF是CFF轮廓,PDF里嵌入性能不如TrueType轮廓的TTF稳定。

3.2 体积优化思路

完整中文字体打进前端bundle,加载就会变慢。实测:思源黑体完整版大约16MB,转换出来的vfs_fonts.js约8-9MB(因为做了Base64编码),前端加载这种文件在4G网络下几乎不可用。

办法是字体子集化。保留你项目里可能用到的常用汉字,一般3500个常用字+数字字母符号就够了,体积能从16MB降到2MB左右,实际体感是打开页面不再卡顿。

我用的工具是fontmin,一个npm包,可以做字体子集化,按字符集裁剪字体文件,保留字形白名单。用法很简单:

npm install -g fontmin

然后创建一个字符集文件,比如chars.txt,里面放你系统里可能出现的所有中文汉字,最少也要把GB2312的6763个汉字放进去:

的一是在不了有和人这中大为上个国我以要他时来用们生到作地于出就分对成会可主发年动同工也能下过子说产种面而方后多定行学法所民得经十三之进着等部度家电力里如水化高自二理起小物现实加量都两体制机当使点从业本去把性好应开它合还因由其些然前外天政四日那社义事平形相全表间样与关各重新线内数正心反你明看原又么利比或但质气第向道命此变条只没结解问意建月公无系军很情者最立代想已通并提直题党程展五果料象员革位入常文总次品式活设及管特件长求老头基资边边路和几则观七山程必许取权持信何诉白百即委每叫部六金界则济料至教务难放议单记早九华六联号称交铁确京除速区院马验带议该南条据车辆必讲手器办九放花受听务资观清反约省劳眼断空其八命较安与做件农张东风士任气决收变北被装整口取转即色式角问七及华信接话集维增米试观义市保修造身更观做格力太军外段山土界技立写往必议科证类运快志满且深走研济治导育济验准规更信据况区持层劳代己基确需斯知拉各海半空边将或金世维战济调务装九立按号产别光根型单何等指思东已验证集示历格适华统路元断精头热志响细信众华层社难供传约先必影何计铁组火南解整角华行里织志观包再始组土流往记思极强率或众维度运并重精决满和且议圆或维深铁结技准接何维共体前长局速集标铁金或指局主比风称极干警必装消别器将南列积军求资治济常者

注意,上面这份字符集是我示例用的,实际项目你应该从数据库的用户真实数据里提取去重,最准确。生成子集化字体:

fontmin SourceHanSansSC-Normal.otf -t chars.txt -o ./subset-fonts

生成的subset/SourceHanSansSC-Normal.ttf就是裁剪后的字体。

4. 实操:pdfmake接入中文字体的完整流程

4.1 将字体转换为vfs_fonts.js

pdfmake官方提供了一个脚本,专门把字体文件转换为项目可引入的JavaScript文件。核心是调用pdfmake自带的build-vfs.js:

cd node_modules/pdfmake node build-vfs.js "你的字体文件路径/SourceHanSansSC-Normal.ttf"

运行完,会在pdfmake目录下生成一个vfs_fonts.js文件,里面是一大段Base64编码。这个文件就是pdfmake在浏览器端的“字体数据库”。

补充一下,很多人到这一步会漏了文件路径。vfs_fonts.js生成后,默认是在node_modules/pdfmake/build/下面,你把这份文件复制到项目的静态资源目录,比如src/assets/下,再手动引入:

import 'pdfmake/build/vfs_fonts';

4.2 注册中文字体

引入之后,需要在pdfmake里明确声明这个字体。关键在pdfMake.fonts配置:

import pdfMake from 'pdfmake/build/pdfmake'; import pdfFonts from 'pdfmake/build/vfs_fonts'; pdfMake.vfs = pdfFonts.pdfMake.vfs; pdfMake.fonts = { SourceHanSans: { normal: 'SourceHanSansSC-Normal.ttf', bold: 'SourceHanSansSC-Normal.ttf', italics: 'SourceHanSansSC-Normal.ttf', bolditalics: 'SourceHanSansSC-Normal.ttf' } };

这段代码的意思:pdfmake内部维护一个vfs对象,key是字体文件名,value是字体文件数据。pdfMake.fonts里定义字体名与文件名的映射。注意,这里的normal、bold等属性的值,必须和vfs对象里的key完全一致。

我没单独准备bold字重,而是直接复用常规字重,因为思源黑体的bold和normal在PDF里视觉差异足够明显,浏览器端如果字体没加载好,伪加粗的效果很差,不如统一用normal字重,再靠PDF渲染端的加粗处理。

4.3 全局默认字体声明

注册完成后,还没完。需要在生成PDF的配置里,把全局默认字体指过去。这一步很多人会漏,漏了就是“一部分中文正常,一部分中文还是乱码”的假象。比如你在表格里没显式指定字体的单元格就是乱码。

生成PDF的核心配置如下:

const docDefinition = { defaultStyle: { font: 'SourceHanSans' }, content: [ // 业务内容 ] }; pdfMake.createPdf(docDefinition).download('测试文件.pdf');

defaultStyle.font的值,必须和pdfMake.fonts里定义的key一致,不能写文件名。我最初就踩过这个坑,写了“SourceHanSansSC-Normal.ttf”,结果全局字体匹配失败,啥也不显示。

4.4 动态内容导出参数处理

如果导出的是动态数据,比如从接口拉取的订单列表,记得把长文本整体包成一个对象,而不是把变量直接拼进模板字符串里:

const orderText = `订单号:${orderId},金额:${amount},时间:${time}`; const docDefinition = { defaultStyle: { font: 'SourceHanSans' }, content: [ { text: orderText, fontSize: 12 } ] };

这样写的好处是,pdfmake能正确识别整段文本的字体,不会因为单个字符的字形映射失败而乱码。如果直接往数组里扔一长串中文,某些版本的pdfmake在vfs查找时,遇到未注册字形会直接跳过,导致部分文字缺失。

5. 文件下载命名的编码坑

很多人在内容不乱码之后,栽在下载文件名上。前端下载PDF用pdfmake的download方法,默认文件名如果是中文,在某些浏览器比如旧版Chrome、Edge、Safari下,下载下来的文件名有可能乱码或者变成一串百分号编码。

这个问题的根源在于浏览器对URL里的中文文件名处理机制,不是pdfmake的问题。解决方案有两种:

第一种,直接用download方法传中文名,这个在多数现代浏览器上已经没问题了:

pdfMake.createPdf(docDefinition).download('月度销售报表.pdf');

第二种,如果你们测试环境有老浏览器,就手动获取blob,再用URL.createObjectURL导出:

pdfMake.createPdf(docDefinition).getBlob((blob) => { const link = document.createElement('a'); link.href = URL.createObjectURL(blob); link.download = '月度销售报表.pdf'; link.click(); URL.revokeObjectURL(link.href); });

后端配合的话,还可以把文件名放到Content-Disposition头里,前端用encodeURIComponent处理一次:

filename*=UTF-8''${encodeURIComponent(filename)}

6. pdfmake常见问题和排查速查表

这块内容是我整理了自己和几个前端群里的同学遇到的典型问题,汇总成一张速查表,边排查边对照用。

问题现象可能原因排查重点
全部中文显示为方块vfs_fonts.js未引入或defaultStyle.font未设置看浏览器Network是否加载了vfs_fonts.js,console里有没有报错
部分中文乱码、部分正常子集化字体漏了字符临时换回完整字体测试,排除是否子集化裁剪问题
表格/列表里中文正常,但某些文字异常局部显式设置的font覆盖了全局搜索content里有没有单独的font属性
导出文件名乱码浏览器兼容问题改用getBlob方式下载,或后端配合Content-Disposition
英文正常但中文间距怪异字体被当成纯拉丁字体处理确认defaultStyle.font是否正确指向中文字体,且vfs_fonts.js已经加载
字体文件太大导致页面加载卡顿使用了完整中文字体用fontmin子集化,3500常用字可以控制在2MB以内
pdfmake.createPdf报错font not foundvfs里没有对应字体文件检查pdfMake.vfs是否已赋值,且key与注册名一致

这些问题是多端、多版本情况下最容易出现的。建议你把vfs_fonts.js放在静态资源目录并加版本号,避免浏览器缓存旧文件导致更新后乱码。

7. 实战案例:将一个管理后台报表导出做到无乱码

接下来我用一个真实项目片段演示完整流程,场景是导出“订单汇总月报”。这个报表包含标题、表格、页脚,总数据量大概几百行,要求导出成PDF后排版清晰、中文不乱码、并能按月份命名。

第一步,初始化项目并安装依赖:

npm install pdfmake fontmin

第二步,准备子集化字体。我直接把订单系统里所有可能出现的中文字符(商品名、地区名、备注等)去重之后做成chars.txt,然后执行:

fontmin ./fonts/SourceHanSansSC-Normal.otf -t ./chars.txt -o ./public/fonts/

第三步,生成vfs_fonts.js并拷贝到项目src目录:

cd node_modules/pdfmake node build-vfs.js "../../public/fonts/SourceHanSansSC-Normal.ttf"

生成的vfs_fonts.js复制到src/assets/pdfmake/下。

第四步,编写导出模块:

import pdfMake from 'pdfmake/build/pdfmake'; import pdfFonts from '../assets/pdfmake/vfs_fonts'; pdfMake.vfs = pdfFonts.pdfMake.vfs; pdfMake.fonts = { SourceHanSans: { normal: 'SourceHanSansSC-Normal.ttf', bold: 'SourceHanSansSC-Normal.ttf', italics: 'SourceHanSansSC-Normal.ttf', bolditalics: 'SourceHanSansSC-Normal.ttf' } }; export function exportMonthlyReport(month, rows) { const tableBody = rows.map(item => [ item.orderId, item.productName, item.region, item.amount.toFixed(2), item.status ]); const docDefinition = { defaultStyle: { font: 'SourceHanSans' }, content: [ { text: `${month}月度订单汇总`, fontSize: 18, alignment: 'center', margin: [0, 0, 0, 16] }, { table: { headerRows: 1, widths: ['auto', '*', 'auto', 'auto', 'auto'], body: [ ['订单编号', '商品名称', '地区', '金额(元)', '状态'], ...tableBody ] } } ], pageSize: 'A4', pageMargins: [40, 60, 40, 60] }; pdfMake.createPdf(docDefinition).download(`${month}订单汇总.pdf`); }

第五步,测试验证。因为导出文件名带了中文,我建议在Windows和macOS下各侧一次下载,确认文件名无乱码。若不放心,就把download参数拆出来单独用encodeURIComponent再做一次。

8. 关于字体加密、压缩和加载性能的补充

中文字体体积大是痛点,除了子集化,还能做几件事减少影响:

第一,把vfs_fonts.js放到CDN,不要打进主bundle里。vfs_fonts.js本质是个纯静态配置文件,放CDN利用浏览器缓存,第二次加载直接命中本地缓存。

第二,如果能接受个别字符用系统字体代替,还可以做成“异步加载”,等用户点导出的时候,动态import这个文件,而不是页面初始化就加载。这个优化对低端手机特别有效。

第三,如果你们应用有权限体系,导出的PDF如果涉及敏感数据,建议后端把PDF流化传输,前端不要直接暴露vfs_fonts.js给页面。不过这是架构层面的权衡,业务没这个要求就不用过度设计。

9. 最后的经验分享

这套方案我前后用了大半年,不同项目里字体选型、打包方式、加载策略来回调了几轮。给新接触pdfmake的同学一条最稳的起步路径:先不管体积,下载思源黑体完整版,直接转vfs_fonts.js,接上defaultStyle.font,等整个链路通了再回来做子集化。这样排查问题的时候能区分是“字体缺失”还是“业务代码逻辑”的问题。

等你把项目交给别人维护时,一定要在代码注释里写清楚:字体是子集化的,新增内容涉及生僻字时需要重新生成chars.txt、重新跑fontmin、重新转vfs_fonts.js。这个细节没写,上线一个月后业务方反馈某个生僻字导出乱码,届时排障成本远大于现在补几行注释的成本。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询