封裝一個好用的頁面導出 PDF 工具 Hook (html2canvas + jspdf)
在最近的一個項目中,遇到一個將頁面內容(詳情頁)導出為 PDF的需求,但是好像目前沒有直接把dom轉成pdf這樣一步到位的技術,所以自己封裝了一個間接轉換的方法,基於 Vue3 + TypeScript 的通用 Hook 封裝,利用 html2canvas 和 jspdf 實現網頁內容導出為 PDF,並解決了 滾動截斷 、 清晰度不足 以及 自動分頁 等常見問題。
一、 技術選型
- html2canvas : 將 DOM 元素轉換為 Canvas 圖片。
- jspdf : 將 Canvas 圖片生成 PDF 文件。
- 封裝 : 使用 Hook 方式封裝,方便複用。
二、 核心痛點與解決方案
在實現過程中,我們通常會遇到以下幾個坑:
- 導出內容不全 :如果頁面有滾動條,直接截圖只能截取可視區域。
- 解法 :在截圖前將 DOM 高度設置為 auto ,並獲取 scrollHeight 傳遞給 html2canvas 的 windowHeight 參數。
- 圖片模糊 :默認截圖出來的 PDF 很模糊。
- 解法 :設置 scale: 2 ,提高 Canvas 的像素密度。
- 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就不會導出這個元素
。 - 最終實現效果
需要導出的頁面
導出的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 上了。
。
