博客 / 詳情

返回

html2canvas + jspdf實現頁面導出成pdf

封裝一個好用的頁面導出 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 即可。

<a-button type="primary" @click="exportPDF" v-if="disabled"> 導出PDF </a-button>
import { useExportPDF } from "/@/hooks/exportpdf/useExportpdf";
// 導出的 DOM 元素
const pdfContainer = ref < HTMLDivElement > 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就會出現跨域問題?
簡單來説就是img標籤只是“展示”數據,而 轉成Canvas 需要“讀取”數據 。瀏覽器的安全策略(同源策略)就是“看一眼”和“拿走數據”的區別
img 標籤展示數據跟把圖片轉成轉成Canvas瀏覽器都會請求圖片,服務器返回圖片數據。區別在於:
1.img 標籤加載

  • 請求頭 :瀏覽器發起請求時, Origin 字段可能不被包含(或者是 null ),或者僅僅作為 Referer 發送。它通常被視為一個“簡單請求”。
  • 響應頭 :服務器返回圖片數據。通常 不需要 包含 Access-Control-Allow-Origin 等 CORS 相關頭信息。
  • 結果 :瀏覽器接收到數據,渲染引擎直接解碼並在屏幕上繪製像素。JavaScript 無法接觸到這些數據。2.開啓 CORS 的情況( crossorigin="anonymous" 或 Canvas 請求)

2.當你為了 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 上了。
user avatar
0 位用戶收藏了這個故事!

發佈 評論

Some HTML is okay.