1. 问题背景与核心痛点
最近在做一个后台管理系统时,遇到了一个典型的需求:需要将页面中的表格数据导出为PDF文件。这个看似简单的功能,在实际开发中却遇到了中文乱码的"拦路虎"。使用流行的jsPDF库时,英文和数字都能正常显示,但一到中文就变成了"口口口"这样的乱码。
这个问题其实困扰过不少前端开发者。jsPDF作为一款轻量级的纯JavaScript PDF生成库,虽然使用方便,但对中文的支持确实存在先天不足。经过多次尝试和查阅资料,我发现这主要与字体缺失和编码处理有关。下面就把我的完整解决方案分享给大家,包括原理分析、具体实现和避坑指南。
2. 技术原理深度解析
2.1 为什么会出现中文乱码?
要解决这个问题,首先需要理解乱码产生的根本原因。PDF文件本质上是一种复杂的文档格式,它对文本的显示有严格要求:
- 字体嵌入机制:PDF要求所有显示文本必须使用嵌入的字体文件,而jsPDF默认只携带了基础的英文ASCII字符集
- 编码方式差异:中文字符通常采用Unicode编码,而PDF内部使用特定的编码映射表(CMAP)
- 字体度量信息:中文等宽字体需要正确的字符宽度信息才能正确排版
2.2 jsPDF的字体处理机制
jsPDF内部使用ttf格式的字体文件,通过将字体转换为JavaScript数组的方式嵌入。默认情况下,它只包含以下字体:
- 标准字体:courier, helvetica, times, symbol
- 这些字体仅支持ISO-8859-1编码(即拉丁字符集)
这就是为什么直接使用doc.text('中文内容', 10, 10)会出现乱码的根本原因。
3. 完整解决方案实现
3.1 方案选型对比
经过调研,解决中文乱码主要有以下几种方案:
| 方案 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| 使用jsPDF自带的亚洲字体 | 简单直接 | 字体选择有限 | 简单需求 |
| 自定义TTF字体 | 灵活自由 | 需要处理字体版权 | 企业级应用 |
| 将文本转为图片 | 兼容性好 | 文件体积大 | 复杂排版 |
| 使用pdf-lib等替代库 | 功能强大 | 学习成本高 | 复杂需求 |
综合考虑后,我选择了自定义TTF字体的方案,因为它既保持了jsPDF的轻量优势,又能灵活支持各种中文字体。
3.2 具体实现步骤
3.2.1 准备字体文件
首先需要获取合法的中文字体文件(.ttf)。这里以"思源黑体"为例:
- 从官方渠道下载字体文件(如SourceHanSansCN-Regular.ttf)
- 使用在线转换工具将ttf转为jsPDF可用的格式
- 推荐使用:https://rawgit.com/MrRio/jsPDF/master/fontconverter/fontconverter.html
- 转换后会得到两个文件:
- 字体定义文件(如SourceHanSansCN-normal.js)
- 度量文件(如SourceHanSansCN-normal.metrics.js)
3.2.2 集成字体到项目中
将生成的两个JS文件放入项目目录,然后在代码中引入:
javascript复制// 引入字体文件
import './fonts/SourceHanSansCN-normal.js';
import './fonts/SourceHanSansCN-normal.metrics.js';
3.2.3 初始化PDF并应用字体
javascript复制// 创建PDF实例
const doc = new jsPDF();
// 添加字体
doc.addFileToVFS('SourceHanSansCN-normal.ttf', SourceHanSansCN);
doc.addFont('SourceHanSansCN-normal.ttf', 'SourceHanSansCN', 'normal');
// 设置字体
doc.setFont('SourceHanSansCN');
// 现在可以正常输出中文了
doc.text('中文内容测试', 10, 10);
3.2.4 表格数据导出实现
结合autoTable插件实现表格导出:
javascript复制// 引入autoTable插件
import 'jspdf-autotable';
// 表格数据
const data = [
{id: 1, name: '张三', department: '研发部'},
{id: 2, name: '李四', department: '市场部'}
];
// 生成PDF
doc.autoTable({
head: [['ID', '姓名', '部门']],
body: data.map(item => [item.id, item.name, item.department]),
styles: {font: 'SourceHanSansCN'}
});
// 保存文件
doc.save('员工数据.pdf');
4. 关键问题与解决方案
4.1 字体文件过大问题
中文字体通常体积较大(3-5MB),这会导致:
- 前端加载缓慢
- PDF文件体积膨胀
优化方案:
- 字体子集化:只包含实际用到的字符
- 使用pyftsubset工具(来自fonttools)
bash复制
pyftsubset SourceHanSansCN.ttf --text-file=used-chars.txt --output-file=subset.ttf - 服务端生成:将PDF生成移到后端处理
- CDN加速:将字体文件放在CDN上
4.2 复杂表格样式问题
当表格包含合并单元格、特殊样式时,autoTable可能不够灵活。
解决方案:
- 手动绘制表格:
javascript复制// 绘制表头
doc.setFillColor(240, 240, 240);
doc.rect(10, 10, 190, 10, 'F');
doc.text('姓名', 15, 15);
doc.text('部门', 100, 15);
// 绘制数据行
data.forEach((item, i) => {
const y = 20 + i * 10;
doc.text(item.name, 15, y);
doc.text(item.department, 100, y);
});
- 使用更高级的表格插件,如pdfmake
4.3 跨平台兼容性问题
不同操作系统和PDF阅读器可能对字体渲染有差异。
应对策略:
- 在所有目标平台上测试
- 使用更通用的字体(如思源黑体)
- 在PDF元数据中声明字体嵌入:
javascript复制doc.setProperties({
title: '员工数据',
creator: '公司HR系统',
producer: 'jsPDF with custom font'
});
5. 性能优化实践
5.1 懒加载字体
只在需要生成PDF时加载字体:
javascript复制async function generatePDF() {
const fontModule = await import('./fonts/SourceHanSansCN-normal.js');
// ...生成PDF逻辑
}
5.2 使用Web Worker
将PDF生成放在Web Worker中,避免阻塞UI:
javascript复制// worker.js
importScripts('jspdf.min.js', 'SourceHanSansCN-normal.js');
self.onmessage = function(e) {
const doc = new jsPDF();
// ...生成PDF
const pdfOutput = doc.output('blob');
self.postMessage(pdfOutput);
};
5.3 缓存机制
缓存已生成的PDF或字体文件:
javascript复制const fontCache = new Map();
function getFont(fontName) {
if(fontCache.has(fontName)) {
return Promise.resolve(fontCache.get(fontName));
}
return import(`./fonts/${fontName}.js`).then(module => {
fontCache.set(fontName, module);
return module;
});
}
6. 实际案例与效果对比
6.1 案例:员工信息导出
需求:
- 导出1000条员工数据
- 包含中文姓名、部门、联系方式
- 需要分页和页眉页脚
实现代码:
javascript复制function exportEmployeeData(data) {
const doc = new jsPDF();
// 设置中文字体
doc.addFileToVFS('SourceHanSansCN-normal.ttf', SourceHanSansCN);
doc.addFont('SourceHanSansCN-normal.ttf', 'SourceHanSansCN', 'normal');
doc.setFont('SourceHanSansCN');
// 添加页眉
doc.setFontSize(16);
doc.text('员工信息表', 105, 15, {align: 'center'});
// 生成表格
doc.autoTable({
head: [['ID', '姓名', '部门', '电话']],
body: data.map(item => [
item.id,
item.name,
item.department,
item.phone
]),
startY: 25,
styles: {font: 'SourceHanSansCN'}
});
// 添加页脚
const pageCount = doc.internal.getNumberOfPages();
for(let i = 1; i <= pageCount; i++) {
doc.setPage(i);
doc.text(`第 ${i} 页/共 ${pageCount} 页`, 105, 285, {align: 'center'});
}
doc.save('员工信息表.pdf');
}
6.2 效果对比
| 指标 | 默认方案 | 优化后方案 |
|---|---|---|
| 中文支持 | 乱码 | 完美显示 |
| 文件大小 | 约50KB | 约300KB(含字体) |
| 生成时间 | 100ms | 500ms(首次) |
| 兼容性 | 仅英文 | 全平台通用 |
7. 进阶技巧与扩展应用
7.1 多语言支持
如果需要支持多种语言,可以注册多个字体:
javascript复制// 注册日语字体
doc.addFileToVFS('NotoSansJP.ttf', NotoSansJP);
doc.addFont('NotoSansJP.ttf', 'NotoSansJP', 'normal');
// 使用不同字体
doc.setFont('SourceHanSansCN');
doc.text('中文内容', 10, 10);
doc.setFont('NotoSansJP');
doc.text('日本語コンテンツ', 10, 20);
7.2 自定义字体样式
通过扩展jsPDF原型实现样式复用:
javascript复制jsPDF.API.myText = function(text, x, y, styles = {}) {
this.setFont('SourceHanSansCN');
this.setFontSize(styles.size || 12);
this.setTextColor(styles.color || '#000000');
this.text(text, x, y);
};
// 使用
doc.myText('自定义样式文本', 10, 10, {size: 16, color: '#FF0000'});
7.3 与服务端结合
对于大量数据,可以考虑前后端分工:
- 前端:收集数据、准备模板
- 后端:使用更强大的PDF库(如PDFKit)生成
- 通过API交互:
javascript复制async function generatePDFOnServer(data) {
const response = await fetch('/api/generate-pdf', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({data})
});
return await response.blob();
}
8. 常见问题排查指南
8.1 字体加载失败
症状: 控制台报错"Font not found"
解决方案:
- 检查字体路径是否正确
- 确认字体文件已正确转换
- 检查字体名称是否一致
8.2 部分字符仍显示乱码
症状: 大多数字符正常,但某些特殊字符显示为方框
原因: 字体子集不完整
解决:
- 检查字符是否包含在字体中
- 重新生成包含全部所需字符的字体子集
- 考虑使用更完整的字体
8.3 性能问题
症状: 生成大量数据时页面卡顿
优化方案:
- 分批次生成
- 使用Web Worker
- 服务端生成
8.4 样式不一致
症状: 在不同设备上显示效果不同
解决方案:
- 在所有目标设备上测试
- 使用更通用的字体
- 考虑将关键内容转为图片
9. 替代方案评估
虽然本文重点介绍jsPDF方案,但根据项目需求,其他方案也可能更合适:
9.1 pdf-lib
javascript复制import { PDFDocument, rgb } from 'pdf-lib';
async function createPDF() {
const pdfDoc = await PDFDocument.create();
const page = pdfDoc.addPage();
// 嵌入字体
const fontBytes = await fetch('SourceHanSansCN.ttf').then(res => res.arrayBuffer());
const customFont = await pdfDoc.embedFont(fontBytes);
// 绘制文本
page.drawText('中文内容', {
x: 50,
y: 500,
size: 15,
font: customFont
});
const pdfBytes = await pdfDoc.save();
download(pdfBytes, 'output.pdf');
}
优点:
- 更现代的API
- 更好的Unicode支持
- 更丰富的功能
缺点:
- 更大的体积
- 更高的学习曲线
9.2 服务端生成
使用Node.js的pdfkit:
javascript复制const PDFDocument = require('pdfkit');
const fs = require('fs');
const doc = new PDFDocument();
doc.pipe(fs.createWriteStream('output.pdf'));
// 使用中文字体
doc.font('SourceHanSansCN.ttf')
.text('中文内容', 100, 100);
doc.end();
适用场景:
- 大量数据处理
- 需要服务器端资源
- 复杂报表生成
10. 项目总结与个人心得
在实际项目中完整实现这个功能后,我总结了以下几点经验:
-
字体选择很重要:不是所有中文字体都能完美工作,测试发现思源黑体、方正字体系列兼容性最好
-
性能要有预期:首次加载字体确实会影响性能,但可以通过预加载或服务端渲染优化
-
移动端适配:在iOS设备上测试时发现某些PDF阅读器渲染效果不同,需要额外调整
-
版本兼容性:jsPDF的不同版本对字体支持有差异,建议锁定版本号
一个特别实用的技巧是:在开发环境使用精简版的字体子集(只包含测试用字),在生产环境再切换完整字体。这样可以大幅提升开发时的热更新速度。
最后提醒一点:商用项目一定要注意字体版权问题。许多美观的中文字体都是商用授权的,务必确认授权后再使用。可以考虑使用开源字体(如思源系列)或购买正版授权。