简介:这份资源面向需要在Web前端或Node环境中导出中文PDF的开发者,尤其是使用pdfmake时遇到中文乱码、字体缺失问题的技术人员。资源包提供了一套可直接运行的示例方案,帮助读者快速验证pdfmake加载中文字体并生成无乱码PDF的完整流程。压缩包共4个文件,约2.36MB,以JavaScript脚本和HTML页面为主,其中脚本文件承担pdfmake核心库与中文字体映射的职责,HTML页面则作为浏览器端的演示入口,另含系统隐藏文件不影响使用。目前已有1313人学习下载,说明该方案在实际开发中具有较高的参考价值。读者拿到资源后,可直接在浏览器中打开演示页面,点击下载即可观察中文PDF的导出效果,并以此为基础替换字体或调整文档结构,将方案迁移到自己的项目中,省去反复排查字体配置与编码问题的时间,适合作为pdfmake中文导出功能的入门与排错参考。
1. 前端导出中文 PDF 的最后一公里:pdfmake 到底能不能扛住
做过管理后台的都知道,导出 PDF 这个需求看着简单,真上手十有八九要翻车。用浏览器原生window.print()吧,样式全丢、分页靠玄学;用后端生成吧,服务器得装字体、装渲染引擎,部署环境一换就崩。于是很多人把目光投向 pdfmake——一个纯前端的 PDF 生成库,声明式文档定义,不依赖后端,浏览器里直接出文件。
但真正让一线开发者头疼的,从来不是“怎么画表格”,而是“中文怎么不乱码”。默认的 Roboto 字体压根没有中文字形,你写进去的汉字要么变成空白方块,要么直接消失。这篇笔记就围绕 pdfmake 导出中文 PDF 无乱码文件这条主线,把字体注册、vfs 打包、构建配置、常见翻车点一次讲透。适合正在做前端导出、被中文乱码卡住的同学,也适合想搞清楚 pdfmake 字体机制、避免重复踩坑的熟手。
2. pdfmake 的中文字体机制:为什么默认导出全是方块
2.1 默认字体 Roboto 的局限与乱码根因
pdfmake 内部维护了一个虚拟文件系统(vfs),默认只打包了 Roboto 系列字体。PDF 本质上是一个自包含的文档格式,它不会去读你系统里装了什么字体,而是要求你把字体文件嵌入到 PDF 里。所以当你的文档里出现中文,而 pdfmake 的 vfs 里没有对应中文字形时,渲染引擎找不到字形,就只能输出空白或者方块。
很多人第一反应是“我在 CSS 里设了中文字体啊”,但 pdfmake 不吃 CSS 那一套。它的字体配置是独立的一套对象,必须在pdfMake.fonts里显式声明,并且字体文件必须能被 pdfmake 访问到——要么通过 vfs 内嵌,要么通过 URL 加载。这就是为什么你明明页面显示正常,导出 PDF 却全是乱码。
理解这一点很关键:pdfmake 的字体是“打包进 PDF”的,不是“引用系统字体”。所以中文字体文件动辄几 MB 到十几 MB,全量打包会让生成的 JS 体积暴涨,这也是后面要讲的按需裁剪的原因。
2.2 字体注册的三种方式与选型对比
实际项目里,把中文字体交给 pdfmake 常见有三种做法,各有适用场景。
第一种是vfs内嵌。把字体文件转成 base64,塞进 pdfmake 的虚拟文件系统。优点是纯前端、无网络依赖、离线可用;缺点是体积大,一个完整中文字体 base64 后可能十几 MB,首屏加载压力明显。
第二种是URL远程加载。在pdfMake.fonts里直接写字体文件的 URL,pdfmake 在生成时去请求。优点是主包体积小;缺点是有网络依赖,跨域没配好会直接失败,而且首次生成有等待。
第三种是服务端预生成字体子集。把项目里实际用到的字符提取出来,用工具生成一个只包含这些字形的子集字体,再走 vfs 内嵌。优点是体积能压到几百 KB;缺点是多了一步构建流程,字符集变了要重新生成。
| 方式 | 体积 | 网络依赖 | 适用场景 |
|---|---|---|---|
| vfs 内嵌全量字体 | 大(10MB+) | 无 | 离线工具、内网系统 |
| URL 远程加载 | 小 | 有 | 公网后台、字体可 CDN 化 |
| 子集字体内嵌 | 小(几百 KB) | 无 | 字符集固定的报表导出 |
我一般会优先推荐子集方案,尤其是导出内容字段相对固定的后台报表。如果字符集不可控(比如用户能输入任意文字),那就退回全量 vfs 内嵌,用体积换稳定。
2.3 字体文件格式的选择:TTF 与 OTF 的差异
pdfmake 底层依赖 pdfkit,对字体格式有要求。实践中 TTF 的兼容性最好,OTF(尤其是 CFF 轮廓的 OTF)在部分版本上会出现解析异常或者字形缺失。所以选字体时,优先找 TTF 版本。
另外要注意字体的授权。很多商业中文字体不允许嵌入分发,用在项目里要确认许可。开源可商用的中文字体里,思源系列、文泉驿系列是常见选择。字体文件本身不要用来源不明的版本,否则可能出现部分汉字缺字,这种问题排查起来非常费劲——因为不是全乱码,而是个别字变方块,很容易被忽略。
3. 从零跑通一个中文 PDF 导出:vfs 打包与字体注册实操
3.1 环境准备与依赖安装
先建一个最小可复现的工程。用 npm 装 pdfmake,注意版本差异:0.1.x 和 0.2.x 在 API 和构建产物上有区别,0.2.x 对模块化更友好。这里以常见的 0.2.x 为例。
# 初始化一个最小工程 mkdir pdfmake-cn-demo && cd pdfmake-cn-demo npm init -y # 安装 pdfmake 和构建工具 npm install pdfmake npm install --save-dev vite装完之后,node_modules/pdfmake/build/下会有pdfmake.min.js和vfs_fonts.js。默认的vfs_fonts.js只含 Roboto,中文场景要替换成自己的字体 vfs。
3.2 用脚本把 TTF 转成 pdfmake 可用的 vfs
pdfmake 官方提供了一个pdfmake-font-generator之类的工具思路,但更通用的做法是自己写个小脚本,把 TTF 读成 base64,拼成 vfs 对象。下面这个脚本把字体文件转成vfs_fonts_cn.js。
// build-vfs.js const fs = require('fs'); const path = require('path'); // 字体文件名,放在项目根目录的 fonts 下 const fontFile = 'SourceHanSansCN-Regular.ttf'; const fontPath = path.join(__dirname, 'fonts', fontFile); // 读取字体并转 base64 const base64 = fs.readFileSync(fontPath).toString('base64'); // pdfmake 的 vfs 结构:文件名 -> base64 内容 const vfs = { [fontFile]: base64, }; // 输出成可直接引入的 JS 文件 const output = `window.pdfMake = window.pdfMake || {}; window.pdfMake.vfs = ${JSON.stringify(vfs)};`; fs.writeFileSync(path.join(__dirname, 'vfs_fonts_cn.js'), output); console.log('vfs 生成完成,大小约', (output.length / 1024 / 1024).toFixed(2), 'MB');这段脚本的逻辑很直白:读字体文件、转 base64、按 pdfmake 要求的文件名 -> base64结构组装、挂到window.pdfMake.vfs上。参数上唯一要改的是fontFile,换成你实际用的 TTF 文件名。生成后控制台会打印体积,如果超过 10MB,就该考虑子集方案了。
提示:base64 会让体积膨胀约 33%,所以一个 8MB 的 TTF 转完接近 11MB,这是正常的,不是脚本写错了。
3.3 注册字体并生成第一份中文 PDF
vfs 有了,接下来在业务代码里注册字体并调用。关键点是pdfMake.fonts里的normal、bold、italics、bolditalics四个键,至少要提供normal,否则默认样式找不到字体。
// main.js import pdfMake from 'pdfmake/build/pdfmake'; import 'pdfmake/build/vfs_fonts'; // 默认 vfs,会被下面覆盖 import './vfs_fonts_cn.js'; // 我们生成的中文字体 vfs // 把自定义 vfs 合并进 pdfMake pdfMake.vfs = { ...pdfMake.vfs, ...window.pdfMake.vfs, }; // 注册中文字体 pdfMake.fonts = { SourceHanSans: { normal: 'SourceHanSansCN-Regular.ttf', bold: 'SourceHanSansCN-Regular.ttf', // 没有粗体文件时先复用 italics: 'SourceHanSansCN-Regular.ttf', bolditalics: 'SourceHanSansCN-Regular.ttf', }, }; // 文档定义 const docDefinition = { defaultStyle: { font: 'SourceHanSans', // 全局指定中文字体 fontSize: 12, }, content: [ { text: '中文导出测试:这是一段包含标点、数字 123 和英文 ABC 的文本。', }, { table: { body: [ ['字段', '值'], ['姓名', '张三'], ['备注', '无乱码测试通过'], ], }, }, ], }; // 生成并下载 pdfMake.createPdf(docDefinition).download('中文导出测试.pdf');逻辑说明:先引入 pdfmake 主包和默认 vfs,再引入自定义 vfs 并合并,避免覆盖掉 Roboto 导致英文也出问题。pdfMake.fonts里把四个样式键都指向同一个中文字体文件,是因为很多中文字体没有独立的粗体、斜体文件,先复用保证不报错。defaultStyle.font设为中文字体名,这样所有内容默认走中文字体。
参数上,fontSize按需调,table的body是二维数组,第一行默认作为表头。生成后如果中文正常显示,说明整条链路通了。
3.4 在构建工具里处理字体体积
上面是同步引入,字体 base64 会直接进主包。用 Vite 或 Webpack 时,更合理的做法是把 vfs 文件做成动态导入,只在用户点击导出时加载。
// 按需加载 vfs,避免首屏体积膨胀 async function exportPdf() { const pdfMakeModule = await import('pdfmake/build/pdfmake'); const pdfMake = pdfMakeModule.default; // 动态加载中文字体 vfs await import('./vfs_fonts_cn.js'); pdfMake.vfs = { ...pdfMake.vfs, ...window.pdfMake.vfs }; pdfMake.fonts = { SourceHanSans: { normal: 'SourceHanSansCN-Regular.ttf', bold: 'SourceHanSansCN-Regular.ttf', italics: 'SourceHanSansCN-Regular.ttf', bolditalics: 'SourceHanSansCN-Regular.ttf', }, }; pdfMake.createPdf({ defaultStyle: { font: 'SourceHanSans', fontSize: 12 }, content: [{ text: '按需加载导出的中文内容' }], }).download('按需导出.pdf'); }这样首屏不背字体体积,只有真正导出时才拉取。代价是首次导出有加载等待,可以在按钮上加个 loading 状态。如果项目对首屏和导出体验都敏感,就回到子集方案,把体积压到几百 KB 再同步引入。
4. 中文乱码排查清单:五个真实翻车现场
4.1 现象:中文全部变方块,英文正常
原因:defaultStyle.font没设成中文字体,或者pdfMake.fonts里注册的字体名和defaultStyle.font对不上。英文正常说明 Roboto 还在生效,中文没有对应字形。
解决:检查pdfMake.fonts的键名和defaultStyle.font的值是否完全一致,大小写敏感。再确认 vfs 里确实有那个字体文件,可以用Object.keys(pdfMake.vfs)打印出来核对。
4.2 现象:部分汉字缺字,个别字变空白
原因:字体文件本身不含这些字形,常见于子集字体生成时字符集没覆盖全,或者用了裁剪过的字体版本。
解决:换用完整版字体,或者重新生成子集时把字符集范围放宽。排查时可以把出问题的字单独拎出来,用字体查看工具确认是否在字形表里。
4.3 现象:控制台报 “File not found” 或字体加载失败
原因:vfs 里的文件名和pdfMake.fonts里写的文件名不一致,或者 vfs 根本没合并进去。
解决:统一文件名,建议用英文名避免编码问题。确认pdfMake.vfs合并顺序正确,自定义 vfs 要在默认 vfs 之后合并。
4.4 现象:本地正常,打包上线后乱码
原因:构建工具把 vfs 文件 tree-shaking 掉了,或者动态导入路径在生产环境不对。
解决:检查构建产物里 vfs 是否真的被打进去,动态导入的路径用相对路径并确认打包配置。必要时把 vfs 文件放到 public 目录用绝对路径引入。
4.5 现象:导出大文档时页面卡死
原因:全量中文字体 base64 解析 + 大量内容渲染,主线程压力大。
解决:改用子集字体,或者把导出放到 Web Worker 里。pdfmake 支持在 worker 中运行,把字体和文档定义传进去,主线程只负责触发和接收结果。
5. 进阶:字体子集裁剪与导出稳定性验证
子集裁剪是中文 PDF 导出里性价比最高的一步。思路很简单:把项目里实际会用到的字符收集起来,生成一个只含这些字形的字体文件。常见做法是用fonttools的pyftsubset命令。
# 安装 fonttools pip install fonttools # 从完整字体裁剪出子集 # --text 指定要保留的字符,--output-file 指定输出 pyftsubset fonts/SourceHanSansCN-Regular.ttf \ --text="中文导出测试字段姓名备注无乱码通过0123456789ABCabc,。:、" \ --output-file=fonts/subset.ttf \ --flavor=woff2参数说明:--text是要保留的字符集合,实际项目里可以从后端接口拉取所有可能的字段值,去重后拼成字符串;--output-file是输出路径;--flavor指定输出格式,pdfmake 场景建议输出 TTF,所以可以去掉--flavor或者改成--flavor=ttf不适用时直接不写。裁剪完再用第 3 章的脚本转 vfs,体积通常能从十几 MB 降到几百 KB。
验证稳定性时,我习惯做三件事。第一,准备一份“字符覆盖测试文档”,把项目里所有可能出现的字符类型都塞进去——中文、英文、数字、标点、生僻字、emoji(如果业务需要),导出后逐页检查。第二,在 Chrome、Firefox、Safari 三个浏览器各跑一遍,pdfmake 在不同浏览器下的字体解析偶有差异。第三,模拟弱网和离线,确认 vfs 内嵌方案不依赖网络。
还有一个容易被忽略的点:PDF 里的字体嵌入状态。用 Adobe Acrobat 或类似工具打开生成的 PDF,查看字体属性,确认中文字体是“嵌入”而不是“引用”。如果是引用,换台机器打开就可能乱码。pdfmake 默认会嵌入 vfs 里的字体,但如果你用了 URL 加载方式,要确认字体确实被嵌进去了。
从那以后我每次做中文导出,都会先跑一遍字符覆盖测试,再确认字体嵌入状态,最后才交付。这个习惯帮我挡掉过好几次“本地好好的,客户那边全是方块”的尴尬。希望帮到你。
本文还有配套的精品资源,点击获取