作为一名长期奋战在前端开发一线的工程师,我深知PDF导出功能在业务场景中的重要性。从报表生成到合同签署,从电子发票到数据存档,PDF作为一种通用文档格式几乎渗透到了所有B端和C端应用中。然而,前端实现PDF导出却是一个让不少开发者头疼的问题——兼容性差、样式错乱、性能低下等问题层出不穷。
在这个系列教程中,我将带大家深度剖析目前最主流的两种前端PDF生成方案:jsPDF和html2canvas。上篇将重点讲解技术选型考量、核心原理剖析以及基础实现方案。不同于网上零散的代码片段,我会结合自己在大中型项目中积累的实战经验,分享那些官方文档不会告诉你的"坑点"和优化技巧。
在前端生成PDF的方案中,大致可以分为三类:
jsPDF+html2canvas的组合属于第一类,其最大优势在于:
但缺点也很明显:
jsPDF本质上是一个PDF文档生成器,其核心流程是:
关键特性包括:
html2canvas的作用是将DOM元素转换为canvas图像,其工作流程为:
需要注意的几点:
首先安装依赖:
bash复制npm install jspdf html2canvas
# 或
yarn add jspdf html2canvas
基础使用示例:
javascript复制import jsPDF from 'jspdf';
import html2canvas from 'html2canvas';
const exportToPDF = async (elementId, filename) => {
const element = document.getElementById(elementId);
const canvas = await html2canvas(element);
const imgData = canvas.toDataURL('image/png');
const pdf = new jsPDF();
const imgProps = pdf.getImageProperties(imgData);
const pdfWidth = pdf.internal.pageSize.getWidth();
const pdfHeight = (imgProps.height * pdfWidth) / imgProps.width;
pdf.addImage(imgData, 'PNG', 0, 0, pdfWidth, pdfHeight);
pdf.save(filename);
};
javascript复制// 纵向A4文档
const pdf = new jsPDF();
// 横向A4文档
const pdf = new jsPDF({
orientation: 'landscape',
unit: 'mm',
format: 'a4'
});
javascript复制pdf.text('Hello World!', 10, 10);
pdf.setFontSize(16);
pdf.setTextColor(255, 0, 0);
javascript复制pdf.setDrawColor(0, 0, 255);
pdf.setFillColor(255, 255, 0);
pdf.rect(20, 20, 50, 50, 'FD'); // F:填充 D:描边
javascript复制pdf.addImage(imgData, 'JPEG', 15, 40, 180, 160);
javascript复制pdf.addPage();
pdf.setPage(2);
javascript复制const options = {
scale: 2, // 缩放倍数,提高清晰度
useCORS: true, // 处理跨域图像
allowTaint: true, // 允许污染画布
logging: true, // 开启日志
backgroundColor: '#ffffff'
};
javascript复制// 忽略特定元素
html2canvas(element, {
ignoreElements: (el) => el.classList.contains('no-export')
});
// 自定义渲染
html2canvas(element, {
onclone: (clonedDoc) => {
clonedDoc.getElementById('header').style.display = 'none';
}
});
默认情况下,jsPDF不支持中文字体。解决方案:
javascript复制import chineseFont from './fonts/simhei-normal';
const pdf = new jsPDF();
pdf.addFileToVFS('simhei-normal.ttf', chineseFont);
pdf.addFont('simhei-normal.ttf', 'simhei', 'normal');
pdf.setFont('simhei');
javascript复制import { autoTable } from 'jspdf-autotable';
pdf.autoTable({
head: [['中文标题']],
body: [['中文内容']],
styles: { font: 'simhei' }
});
html2canvas渲染时常见的样式问题:
css复制/* 错误 */
.position-fixed {
position: fixed;
}
/* 正确 */
.position-absolute {
position: absolute;
}
javascript复制html2canvas(element, {
onclone: (doc) => {
doc.querySelectorAll('[style*="background"]').forEach(el => {
el.style.backgroundImage = 'none';
});
}
});
javascript复制async function exportLargeDoc() {
const pdf = new jsPDF();
const sections = document.querySelectorAll('.export-section');
for (let i = 0; i < sections.length; i++) {
const canvas = await html2canvas(sections[i]);
pdf.addImage(canvas, 'JPEG', 10, 10, 180, 0);
if (i < sections.length - 1) pdf.addPage();
}
pdf.save('large-document.pdf');
}
javascript复制// worker.js
self.importScripts('html2canvas.js');
self.onmessage = async (e) => {
const canvas = await html2canvas(e.data.element);
self.postMessage(canvas.toDataURL());
};
// 主线程
const worker = new Worker('worker.js');
worker.postMessage({ element: document.getElementById('content') });
对于数据表格,推荐使用jsPDF-AutoTable插件:
javascript复制import 'jspdf-autotable';
const data = [
['姓名', '年龄', '城市'],
['张三', '28', '北京'],
['李四', '32', '上海']
];
pdf.autoTable({
head: [data[0]],
body: data.slice(1),
margin: { top: 20 },
styles: { fontSize: 10 },
columnStyles: { 0: { cellWidth: 40 } }
});
处理多页复杂文档的建议方案:
示例代码:
javascript复制const pageHeight = pdf.internal.pageSize.height;
let yPos = 20;
contentBlocks.forEach(block => {
const blockHeight = calculateBlockHeight(block);
if (yPos + blockHeight > pageHeight - 20) {
pdf.addPage();
yPos = 20;
addHeaderFooter(pdf);
}
renderBlock(pdf, block, yPos);
yPos += blockHeight + 10;
});
结合用户输入生成动态PDF:
javascript复制document.getElementById('generate-btn').addEventListener('click', () => {
const userName = document.getElementById('name-input').value;
const userEmail = document.getElementById('email-input').value;
const pdf = new jsPDF();
pdf.text(`姓名: ${userName}`, 20, 20);
pdf.text(`邮箱: ${userEmail}`, 20, 30);
const signature = document.getElementById('signature-pad');
html2canvas(signature).then(canvas => {
pdf.addImage(canvas, 'PNG', 20, 50, 100, 40);
pdf.save('user-profile.pdf');
});
});
检查元素是否设置了overflow:hidden,移除或改为visible
确保跨域资源服务器配置了正确的CORS头,或使用代理方案
javascript复制// 提高分辨率
html2canvas(element, {
scale: 2,
dpi: 300,
letterRendering: true
});
javascript复制function isFeatureSupported() {
try {
new Blob([JSON.stringify({ test: true })], { type: 'application/json' });
return true;
} catch (e) {
return false;
}
}
javascript复制if (!isFeatureSupported()) {
showModal('您的浏览器不支持PDF导出功能,请使用Chrome或Firefox最新版本');
}
在实际项目中,我发现最稳定的方案是结合服务端生成作为fallback。当客户端生成失败时,自动切换到服务端API,虽然会增加服务器负载,但能确保功能可用性。