利用html2canvas+jspdf實(shí)現(xiàn)頁面導(dǎo)出成pdf功能源碼
封裝一個(gè)好用的頁面導(dǎo)出 PDF 工具 Hook (html2canvas + jspdf)
在最近的一個(gè)項(xiàng)目中,遇到一個(gè)將頁面內(nèi)容(詳情頁)導(dǎo)出為 PDF的需求,但是好像目前沒有直接把dom轉(zhuǎn)成pdf這樣一步到位的技術(shù),所以自己封裝了一個(gè)間接轉(zhuǎn)換的方法,基于 Vue3 + TypeScript 的通用 Hook 封裝,利用 html2canvas 和 jspdf 實(shí)現(xiàn)網(wǎng)頁內(nèi)容導(dǎo)出為 PDF,并解決了 滾動(dòng)截?cái)?、 清晰度不足 以及 自動(dòng)分頁 等常見問題。
一、 技術(shù)選型
- html2canvas : 將 DOM 元素轉(zhuǎn)換為 Canvas 圖片。
- jspdf : 將 Canvas 圖片生成 PDF 文件。
- 封裝 : 使用 Hook 方式封裝,方便復(fù)用。
二、 核心痛點(diǎn)與解決方案
在實(shí)現(xiàn)過程中,我們通常會(huì)遇到以下幾個(gè)坑:
- 導(dǎo)出內(nèi)容不全 :如果頁面有滾動(dòng)條,直接截圖只能截取可視區(qū)域。
- 解法 :在截圖前將 DOM 高度設(shè)置為 auto ,并獲取 scrollHeight 傳遞給 html2canvas 的 windowHeight 參數(shù)。
- 圖片模糊 :默認(rèn)截圖出來的 PDF 很模糊。
- 解法 :設(shè)置 scale: 2 ,提高 Canvas 的像素密度。
- PDF 分頁問題 :長圖直接放入 PDF 會(huì)被壓縮變形。
- 解法 :計(jì)算內(nèi)容高度與 A4 紙高度的比例,通過循環(huán) addPage() 實(shí)現(xiàn)自動(dòng)分頁切割。
三、 源碼實(shí)現(xiàn)
新建文件 useExportPdf.ts,下載依賴 html2canvas 和 jspdf 然后引入:
import html2canvas from 'html2canvas';
import jsPDF from 'jspdf';
/**
* 導(dǎo)出頁面為 PDF
* @param dom 需要導(dǎo)出的 DOM 元素
* @param fileName 導(dǎo)出的文件名(不含后綴)
*/
export const useExportPDF = async (dom: HTMLElement, fileName: string) => {
const element = dom;
if (!element) {
console.error('導(dǎo)出失敗,未找到導(dǎo)出元素');
return;
}
// 1. 解決滾動(dòng)截?cái)鄦栴}:獲取元素實(shí)際高度
const originalHeight = element.scrollHeight;
// 臨時(shí)設(shè)置高度為 auto,確保能截取到所有內(nèi)容
const originalStyleHeight = element.style.height;
element.style.height = 'auto';
try {
// 2. 將 DOM 轉(zhuǎn)換為 Canvas
const canvas = await html2canvas(element, {
useCORS: true, // 允許跨域圖片
scale: 2, // 2倍縮放,解決模糊問題
scrollY: -window.scrollY, // 修正滾動(dòng)條偏移
scrollX: 0,
windowHeight: originalHeight, // 告訴 html2canvas 完整高度
});
// 3. 初始化 PDF 實(shí)例
// p: 縱向, mm: 單位毫米, a4: 紙張格式
const pdf = new jsPDF('p', 'mm', 'a4');
// A4 紙內(nèi)容寬度(留邊距)
const imgWidth = 190;
// 根據(jù)寬度計(jì)算等比例的高度
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; // 這里簡化處理,按頁面高度分頁
// 如果內(nèi)容高度超過一頁,循環(huán)添加新頁
while (position < imgHeight) {
pdf.addPage();
// 移動(dòng)圖片位置,實(shí)現(xiàn)視覺上的“接續(xù)”
// 注意:這里簡單的 position += pageHeight 可能需要根據(jù)實(shí)際情況調(diào)整,
// 比如減去一些邊距來防止文字被切斷,Demo 中使用了簡化的邏輯。
pdf.addImage(canvas, 'PNG', 10, 10 - position, imgWidth, imgHeight);
position += pdfPageHeight;
}
// 5. 保存文件
pdf.save(`${fileName}.pdf`);
} catch (error) {
console.error('導(dǎo)出 PDF 異常:', error);
} finally {
// 6. 恢復(fù)原始樣式
element.style.height = originalStyleHeight;
}
};
四、 如何在組件中使用
在 Vue 組件中,我們只需要獲取到 DOM 引用,然后調(diào)用這個(gè) Hook 即可。
<a-button type="primary" @click="exportPDF" v-if="disabled"> 導(dǎo)出PDF </a-button>
import { useExportPDF } from '/@/hooks/exportpdf/useExportpdf';
// 導(dǎo)出的 DOM 元素
const pdfContainer = ref<HTMLDivElement>(null);
// 導(dǎo)出
const exportPDF = async () => {
loading.value = true;
try {
await useExportPDF(pdfContainer.value, 'xxxxpdf');
loading.value = false;
} catch (e) {
console.log(e);
loading.value = false;
}
};
五、 值得注意的事項(xiàng)
- html2canvas 將dom元素轉(zhuǎn)成canvas圖片的時(shí)候如果dom元素中有圖片,需要解決跨域問題,這個(gè)一般來講可以在服務(wù)端(圖片源)設(shè)置 :
圖片的響應(yīng)頭(Response Header)必須包含 CORS 頭,允許你的域名訪問,或者Nginx配置下代理,或者前端解決的話就把圖片轉(zhuǎn)成Base64格式,但是如果圖片比較多,就不建議前端解決了,第一個(gè)因?yàn)檗D(zhuǎn)圖片格式圖片一多就消耗更多時(shí)間,第二個(gè)是因?yàn)檗D(zhuǎn)成Base64格式的圖片會(huì)增加文件大小。 - 還有一個(gè)就是如果你想導(dǎo)致的內(nèi)容之中,有些是不用導(dǎo)出,或者根據(jù)不同條件來區(qū)分是否導(dǎo)出可以使用 data-html2canvas-ignore這個(gè)屬性,設(shè)置為true就不會(huì)導(dǎo)出這個(gè)元素
。
- 最終實(shí)現(xiàn)效果
需要導(dǎo)出的頁面
導(dǎo)出的pdf
六、 總結(jié)
通過這個(gè)封裝,我們實(shí)現(xiàn)了一個(gè)輕量級(jí)且功能完備的 PDF 導(dǎo)出工具。它不僅解決了最讓人頭疼的 長頁面截?cái)?問題,還通過 scale 參數(shù)保證了導(dǎo)出的清晰度。
七、 小思考
為啥img標(biāo)簽就能通過圖片url加載圖片,但是把圖片轉(zhuǎn)成Canvas就會(huì)出現(xiàn)跨域問題?
簡單來說就是 標(biāo)簽只是“展示”數(shù)據(jù),而 轉(zhuǎn)成Canvas 需要“讀取”數(shù)據(jù) 。瀏覽器的安全策略(同源策略)就是“看一眼”和“拿走數(shù)據(jù)”的區(qū)別
標(biāo)簽展示數(shù)據(jù)跟把圖片轉(zhuǎn)成轉(zhuǎn)成Canvas瀏覽器都會(huì)請求圖片,服務(wù)器返回圖片數(shù)據(jù)。區(qū)別在于:
1. 標(biāo)簽加載
請求頭 :瀏覽器發(fā)起請求時(shí), Origin 字段可能不被包含(或者是 null ),或者僅僅作為 Referer 發(fā)送。它通常被視為一個(gè)“簡單請求”。
響應(yīng)頭 :服務(wù)器返回圖片數(shù)據(jù)。通常 不需要 包含 Access-Control-Allow-Origin 等 CORS 相關(guān)頭信息。
結(jié)果 :瀏覽器接收到數(shù)據(jù),渲染引擎直接解碼并在屏幕上繪制像素。JavaScript 無法接觸到這些數(shù)據(jù)。
2.開啟 CORS 的情況( crossorigin="anonymous" 或 Canvas 請求)
當(dāng)你為了 Canvas 導(dǎo)出而給圖片添加 crossorigin 屬性,或者使用 JS fetch 請求圖片時(shí):請求頭 :瀏覽器 強(qiáng)制添加 Origin: https://你的域名.com 字段,明確告訴服務(wù)器是誰在請求。
響應(yīng)頭(關(guān)鍵差別) :
- 如果服務(wù)器支持跨域 :必須返回 Access-Control-Allow-Origin: * 或 Access-Control-Allow-Origin: https://你的域名.com 。
- 如果服務(wù)器不支持 :服務(wù)器可能正常返回了圖片數(shù)據(jù)(狀態(tài)碼 200),但 缺少了 CORS 響應(yīng)頭 。
結(jié)果 :
- 如果 有 CORS 頭:瀏覽器認(rèn)為這份數(shù)據(jù)是“安全”的,允許 Canvas 讀取和導(dǎo)出。
- 如果 沒有 CORS 頭:雖然數(shù)據(jù)下載下來了,但瀏覽器(網(wǎng)絡(luò)層或渲染層)會(huì) 攔截 這次加載,報(bào)錯(cuò) CORS policy ,圖片甚至可能直接裂開(加載失?。?,更別說畫到 Canvas 上了。
到此這篇關(guān)于利用html2canvas+jspdf實(shí)現(xiàn)頁面導(dǎo)出成pdf功能源碼的文章就介紹到這了,更多相關(guān)html2canvas+jspdf頁面導(dǎo)出pdf內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
如何實(shí)現(xiàn)小程序與小程序之間的跳轉(zhuǎn)
這篇文章主要給大家介紹了關(guān)于如何實(shí)現(xiàn)小程序與小程序之間的跳轉(zhuǎn)的相關(guān)資料,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2020-11-11
JavaScrip中window.dispatchEvent的原理和使用具體實(shí)例
window.dispatchEvent是JavaScript中用于手動(dòng)觸發(fā)某個(gè)事件的方法,下面這篇文章主要介紹了JavaScrip中window.dispatchEvent的原理和使用的相關(guān)資料,文中通過代碼介紹的非常詳細(xì),需要的朋友可以參考下2025-04-04
JS+CSS實(shí)現(xiàn)可拖動(dòng)的彈出提示框
這篇文章主要介紹了JS+CSS實(shí)現(xiàn)可拖動(dòng)的彈出提示框,涉及針對(duì)鼠標(biāo)事件及html元素的操作技巧,非常具有實(shí)用價(jià)值,需要的朋友可以參考下2015-02-02
JavaScript動(dòng)態(tài)提示輸入框輸入字?jǐn)?shù)的方法
這篇文章主要介紹了JavaScript動(dòng)態(tài)提示輸入框輸入字?jǐn)?shù)的方法,實(shí)例分析了javascript針對(duì)頁面元素的動(dòng)態(tài)操作技巧,具有一定參考借鑒價(jià)值,需要的朋友可以參考下2015-07-07
JS實(shí)現(xiàn)網(wǎng)頁上隨機(jī)產(chǎn)生超鏈接地址的方法
這篇文章主要介紹了JS實(shí)現(xiàn)網(wǎng)頁上隨機(jī)產(chǎn)生超鏈接地址的方法,涉及JavaScript隨機(jī)數(shù)的相關(guān)使用技巧,具有一定參考借鑒價(jià)值,需要的朋友可以參考下2015-11-11
JS實(shí)現(xiàn)批量上傳文件并顯示進(jìn)度功能
這篇文章主要介紹了JS實(shí)現(xiàn)批量上傳文件并顯示進(jìn)度功能,非常不錯(cuò),具有參考借鑒價(jià)值,需要的朋友可以參考下2017-06-06
微信小程序遍歷Echarts圖表實(shí)現(xiàn)多個(gè)餅圖
這篇文章主要介紹了微信小程序遍歷Echarts圖表實(shí)現(xiàn)多個(gè)餅圖,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2019-04-04

