Vue3 TypeScript html2canvas jspdf 页面导出PDF实现

您当前的位置:   首页 首页 > 技术学堂
Vue3 TypeScript html2canvas jspdf 页面导出PDF实现
发布时间:2026-03-01 04:20:21

封装一个好用的页面导出 PDF 工具 Hook (html2canvas + jspdf)

在最近的一个项目中,遇到一个将页面内容(详情页)导出为 PDF的需求,但是好像目前没有直接把dom转成pdf这样一步到位的技术,所以自己封装了一个间接转换的方法,基于 Vue3 + TypeScript 的通用 Hook 封装,利用 html2canvas 和 jspdf 实现网页内容导出为 PDF,并解决了 滚动截断 、 清晰度不足 以及 自动分页 等常见问题。

一、 技术选型

  • html2canvas : 将 DOM 元素转换为 Canvas 图片。
  • jspdf : 将 Canvas 图片生成 PDF 文件。
  • 封装 : 使用 Hook 方式封装,方便复用。

二、 核心痛点与解决方案

在实现过程中,我们通常会遇到以下几个坑:

  1. 导出内容不全 :如果页面有滚动条,直接截图只能截取可视区域。
    • 解法 :在截图前将 DOM 高度设置为 auto ,并获取 scrollHeight 传递给 html2canvas 的 windowHeight 参数。
  2. 图片模糊 :默认截图出来的 PDF 很模糊。
    • 解法 :设置 scale: 2 ,提高 Canvas 的像素密度。
  3. PDF 分页问题 :长图直接放入 PDF 会被压缩变形。
    • 解法 :计算内容高度与 A4 纸高度的比例,通过循环 addPage() 实现自动分页切割。

三、 源码实现

新建文件 useExportPdf.ts,下载依赖 html2canvas 和 jspdf 然后引入:

import html2canvas from 'html2canvas';
import jsPDF from 'jspdf';

/**
 * 导出页面为 PDF
 * @param dom 需要导出的 DOM 元素
 * @param fileName 导出的文件名(不含后缀)
 */
export const useExportPDF = async (dom: HTMLElement, fileName: string) => {
  const element = dom;
  if (!element) {
    console.error('导出失败,未找到导出元素');
    return;
  }

  // 1. 解决滚动截断问题:获取元素实际高度
  const originalHeight = element.scrollHeight;
  // 临时设置高度为 auto,确保能截取到所有内容
  const originalStyleHeight = element.style.height;
  element.style.height = 'auto';

  try {
    // 2. 将 DOM 转换为 Canvas
    const canvas = await html2canvas(element, {
      useCORS: true, // 允许跨域图片
      scale: 2,      // 2倍缩放,解决模糊问题
      scrollY: -window.scrollY, // 修正滚动条偏移
      scrollX: 0,
      windowHeight: originalHeight, // 告诉 html2canvas 完整高度
    });

    // 3. 初始化 PDF 实例
    // p: 纵向, mm: 单位毫米, a4: 纸张格式
    const pdf = new jsPDF('p', 'mm', 'a4');
    
    // A4 纸内容宽度(留边距)
    const imgWidth = 190; 
    // 根据宽度计算等比例的高度
    const imgHeight = (canvas.height * imgWidth) / canvas.width;
    
    // 获取 PDF 页面可用高度
    const pdfPageHeight = pdf.internal.pageSize.getHeight(); 
    
    // 4. 处理分页逻辑
    let position = 0;
    
    // 第一页
    pdf.addImage(canvas, 'PNG', 10, 10 - position, imgWidth, imgHeight);
    position += pdfPageHeight; // 这里简化处理,按页面高度分页

    // 如果内容高度超过一页,循环添加新页
    while (position < imgHeight) {
      pdf.addPage();
      // 移动图片位置,实现视觉上的“接续”
      // 注意:这里简单的 position += pageHeight 可能需要根据实际情况调整,
      // 比如减去一些边距来防止文字被切断,Demo 中使用了简化的逻辑。
      pdf.addImage(canvas, 'PNG', 10, 10 - position, imgWidth, imgHeight);
      position += pdfPageHeight;
    }

    // 5. 保存文件
    pdf.save(`${fileName}.pdf`);
  } catch (error) {
    console.error('导出 PDF 异常:', error);
  } finally {
    // 6. 恢复原始样式
    element.style.height = originalStyleHeight;
  }
};

四、 如何在组件中使用

在 Vue 组件中,我们只需要获取到 DOM 引用,然后调用这个 Hook 即可。

   导出PDF 
import { useExportPDF } from '/@/hooks/exportpdf/useExportpdf';
// 导出的 DOM 元素
const pdfContainer = ref(null); 
// 导出
      const exportPDF = async () => {
        loading.value = true;
        try {
          await useExportPDF(pdfContainer.value, 'xxxxpdf');
          loading.value = false;
        } catch (e) {
          console.log(e);
          loading.value = false;
        }
      };

五、 值得注意的事项

  • html2canvas 将dom元素转成canvas图片的时候如果dom元素中有图片,需要解决跨域问题,这个一般来讲可以在服务端(图片源)设置 :
    图片的响应头(Response Header)必须包含 CORS 头,允许你的域名访问,或者Nginx配置下代理,或者前端解决的话就把图片转成Base64格式,但是如果图片比较多,就不建议前端解决了,第一个因为转图片格式图片一多就消耗更多时间,第二个是因为转成Base64格式的图片会增加文件大小。
  • 还有一个就是如果你想导致的内容之中,有些是不用导出,或者根据不同条件来区分是否导出可以使用 data-html2canvas-ignore这个属性,设置为true就不会导出这个元素img
  • 最终实现效果
    需要导出的页面
    img
    导出的pdf
    img

六、 总结

通过这个封装,我们实现了一个轻量级且功能完备的 PDF 导出工具。它不仅解决了最让人头疼的 长页面截断 问题,还通过 scale 参数保证了导出的清晰度。

七、 小思考

为啥img标签就能通过图片url加载图片,但是把图片转成Canvas就会出现跨域问题?
简单来说就是Vue3 TypeScript html2canvas jspdf 页面导出PDF实现(图4) 标签只是“展示”数据,而 转成Canvas 需要“读取”数据 。浏览器的安全策略(同源策略)就是“看一眼”和“拿走数据”的区别
Vue3 TypeScript html2canvas jspdf 页面导出PDF实现(图4) 标签展示数据跟把图片转成转成Canvas浏览器都会请求图片,服务器返回图片数据。区别在于:
1.Vue3 TypeScript html2canvas jspdf 页面导出PDF实现(图4) 标签加载

  • 请求头 :浏览器发起请求时, Origin 字段可能不被包含(或者是 null ),或者仅仅作为 Referer 发送。它通常被视为一个“简单请求”。

  • 响应头 :服务器返回图片数据。通常 不需要 包含 Access-Control-Allow-Origin 等 CORS 相关头信息。

  • 结果 :浏览器接收到数据,渲染引擎直接解码并在屏幕上绘制像素。JavaScript 无法接触到这些数据。
    2.开启 CORS 的情况( crossorigin="anonymous" 或 Canvas 请求)
    当你为了 Canvas 导出而给图片添加 crossorigin 属性,或者使用 JS fetch 请求图片时:

  • 请求头 :浏览器 强制添加 Origin: https://你的域名.com 字段,明确告诉服务器是谁在请求。

  • 响应头(关键差别) :

    • 如果服务器支持跨域 :必须返回 Access-Control-Allow-Origin: * 或 Access-Control-Allow-Origin: https://你的域名.com 。
    • 如果服务器不支持 :服务器可能正常返回了图片数据(状态码 200),但 缺少了 CORS 响应头 。
  • 结果 :

    • 如果 有 CORS 头:浏览器认为这份数据是“安全”的,允许 Canvas 读取和导出。
    • 如果 没有 CORS 头:虽然数据下载下来了,但浏览器(网络层或渲染层)会 拦截 这次加载,报错 CORS policy ,图片甚至可能直接裂开(加载失败),更别说画到 Canvas 上了。