一、 前言:为什么 PDF 导出总是前端的噩梦?
在 Vue3 的企业级开发中,我们经常遇到需要将合同、报表或简历导出为 PDF 的需求。 最开始,我们可能觉得 window.print() 就够了,但很快你会发现:
- 不可控:页眉页脚去不掉,浏览器弹窗不美观。
- 截断痛点:一张漂亮的报表图片,刚好被切成了上下两半,分布在两页 PDF 里。
- 样式玄学:在 Chrome 里好好的,到了 360 浏览器或移动端 WebView,字体样式直接原地爆炸。
最近我在项目中深度打磨了一套基于 Vue3 + html2canvas / Snapdom 的方案,踩了无数坑后总结出一套“保姆级”指南。
二、 技术选型:html2canvas 还是 Snapdom?
在决定动手前,我们要明白这两个库的区别:
| 维度 | html2canvas | Snapdom (及其变体) |
|---|---|---|
| 原理 | 遍历 DOM,通过 Canvas 模拟绘图 | 将 DOM 节点转换为原生的 SVG/Canvas 描述 |
| 渲染能力 | 兼容性极强,资料多 | 速度快,对复杂 CSS3 支持更好 |
| 清晰度 | 依赖缩放倍率设置 | 原生支持较好 |
| 分页处理 | 需要手动计算坐标 | 需要配合 jsPDF 逻辑 |
三、 核心避坑指南(干货区)
1. 解决 360 浏览器/低版本核心样式错乱
很多人发现,导出后的 PDF 文字重叠或样式不对。这通常是因为 html2canvas 对某些 CSS 属性(如 flex-gap, filter)支持有限。
- 解决方案:在导出逻辑中,先将复杂的 CSS 属性降级处理,或者手动指定
useCORS: true和allowTaint: false。
2. 强制分页逻辑:不再让内容被“腰斩”
这是最核心的问题。我们需要在 HTML 中定义一个特定的标签,比如 <div page-break></div>,告诉程序这里必须另起一页。
TypeScript
// 定义强制分页标志
const PAGE_BREAK_TAG = 'page-break';
// 递归计算 DOM 高度,遇到带有 page-break 属性的元素强制截断
const handlePagination = (element: HTMLElement) => {
const elements = element.querySelectorAll(`[${PAGE_BREAK_TAG}]`);
elements.forEach(el => {
// 逻辑:计算当前元素在 Canvas 中的 y 坐标,不足一页时补齐空白
// ...
});
};
3. 完美居中:拒绝 PDF 内容“靠边站”
导出后的 PDF 往往是 A4 纸张,如果你的 HTML 内容宽度不足,导出来的效果非常难看。
- 技巧:在生成
jsPDF实例时,通过(pdfWidth - contentWidth) / 2动态计算偏移量。
四、 终极封装:一个 Vue3 Hook 搞定导出
建议将逻辑封装成一个 useExportPDF 的 Hook,让代码更优雅。
TypeScript
import html2canvas from 'html2canvas';
import jsPDF from 'jspdf';
export const useExportPDF = () => {
const exportPDF = async (elementId: string, fileName: string) => {
const element = document.getElementById(elementId);
if (!element) return;
// 1. 提升清晰度:增加 scale
const canvas = await html2canvas(element, {
scale: 2,
useCORS: true,
logging: false
});
const contentWidth = canvas.width;
const contentHeight = canvas.height;
// 2. A4 纸张比例计算
const pageHeight = (contentWidth / 592.28) * 841.89;
let leftHeight = contentHeight;
let position = 0;
const imgWidth = 595.28;
const imgHeight = (592.28 / contentWidth) * contentHeight;
const pageData = canvas.toDataURL('image/jpeg', 1.0);
const pdf = new jsPDF('p', 'pt', 'a4');
// 3. 循环分页逻辑
if (leftHeight < pageHeight) {
pdf.addImage(pageData, 'JPEG', 0, 0, imgWidth, imgHeight);
} else {
while (leftHeight > 0) {
pdf.addImage(pageData, 'JPEG', 0, position, imgWidth, imgHeight);
leftHeight -= pageHeight;
position -= 841.89;
if (leftHeight > 0) pdf.addPage();
}
}
pdf.save(`${fileName}.pdf`);
};
return { exportPDF };
};
五、 结语
HTML 转 PDF 看起来简单,实则全是细节。
- 图片跨域是第一大坑。
- 分页截断是第一大痛点。
- 设备像素比 (DPR) 是模糊的根源。
希望这篇文章能帮你少走弯路。如果你在实现 Snapdom 替代 html2canvas 的过程中遇到了样式不居中的问题,欢迎在评论区一起讨论!