从 html2canvas 到 Snapdom:彻底搞定 Vue3 项目中 HTML 转 PDF 的分页与样式“深坑”

42 阅读2分钟

一、 前言:为什么 PDF 导出总是前端的噩梦?

在 Vue3 的企业级开发中,我们经常遇到需要将合同、报表或简历导出为 PDF 的需求。 最开始,我们可能觉得 window.print() 就够了,但很快你会发现:

  • 不可控:页眉页脚去不掉,浏览器弹窗不美观。
  • 截断痛点:一张漂亮的报表图片,刚好被切成了上下两半,分布在两页 PDF 里。
  • 样式玄学:在 Chrome 里好好的,到了 360 浏览器或移动端 WebView,字体样式直接原地爆炸。

最近我在项目中深度打磨了一套基于 Vue3 + html2canvas / Snapdom 的方案,踩了无数坑后总结出一套“保姆级”指南。

二、 技术选型:html2canvas 还是 Snapdom?

在决定动手前,我们要明白这两个库的区别:

维度html2canvasSnapdom (及其变体)
原理遍历 DOM,通过 Canvas 模拟绘图将 DOM 节点转换为原生的 SVG/Canvas 描述
渲染能力兼容性极强,资料多速度快,对复杂 CSS3 支持更好
清晰度依赖缩放倍率设置原生支持较好
分页处理需要手动计算坐标需要配合 jsPDF 逻辑

三、 核心避坑指南(干货区)

1. 解决 360 浏览器/低版本核心样式错乱

很多人发现,导出后的 PDF 文字重叠或样式不对。这通常是因为 html2canvas 对某些 CSS 属性(如 flex-gap, filter)支持有限。

  • 解决方案:在导出逻辑中,先将复杂的 CSS 属性降级处理,或者手动指定 useCORS: trueallowTaint: 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 看起来简单,实则全是细节。

  1. 图片跨域是第一大坑。
  2. 分页截断是第一大痛点。
  3. 设备像素比 (DPR) 是模糊的根源。

希望这篇文章能帮你少走弯路。如果你在实现 Snapdom 替代 html2canvas 的过程中遇到了样式不居中的问题,欢迎在评论区一起讨论!