vue3实现html转成pdf并导出的示例代码
作者:diang
这篇文章主要为大家详细介绍了如何使用vue3实现html转成pdf并导出功能,文中的示例代码讲解详细,感兴趣的小伙伴可以跟随小编一起学习一下
方案一:html2canvas + jspdf
代码
import html2Canvas from "html2canvas"; import JsPDF from "jspdf"; import moment from "moment"; import axios from "axios"; var noTableHeight = 0; //table外的元素高度 /** * @param title pdf名称 * @param dom 想要打印的元素 * @param fileList 想要打印的元素数组(同类名元素) * @param type 纵向还是横向 * @param isDownLoad 转换完成是否下载 */ export const htmlPdf = async (title, dom, fileList, type, isDownLoad) => { return new Promise((resolve) => { const html = document.querySelector(dom); let htmlHeight = 0; for (let i = 0; i < fileList.length; i++) { htmlHeight = htmlHeight + fileList[i].offsetHeight + 30; } html.style.height = htmlHeight + "px"; const divParentHeight = JSON.parse( JSON.stringify(document.querySelector(dom).offsetHeight) ); let uploadUrl = ""; if (fileList) { const pageHeight = Math.floor((277 * html.scrollWidth) / 190) + 20; //计算pdf高度 for (let i = 0; i < fileList.length; i++) { //循环获取的元素 const multiple = Math.ceil( (fileList[i].offsetTop + fileList[i].offsetHeight) / pageHeight ); //元素的高度 if (isSplit(fileList, i, multiple * pageHeight)) { //计算是否超出一页 var _H = ""; //向pdf插入空白块的内容高度 if (fileList[i].localName !== "tr") { //判断是不是表格里的内容 _H = multiple * pageHeight - (fileList[i].offsetTop + fileList[i].offsetHeight); } else { _H = multiple * pageHeight - (fileList[i].offsetTop + fileList[i].offsetHeight + noTableHeight) + 20; } var newNode = getFooterElement(_H); //向pdf插入空白块的内容 const divParent = fileList[i].parentNode; // 获取该div的父节点 const next = fileList[i].nextSibling; // 获取div的下一个兄弟节点 const isTruncate = true; // 判断兄弟节点是否存在 if (next && !isTruncate) { // 存在则将新节点插入到div的下一个兄弟节点之前,即div之后 divParent.insertBefore(newNode, next); document.querySelector(dom).style.height = newNode.offsetHeight + divParentHeight + "px"; } else { // 否则向节点添加最后一个子节点 divParent.appendChild(newNode); } } } } html2Canvas(html, { allowTaint: false, taintTest: false, logging: false, useCORS: true, dpi: window.devicePixelRatio * 1, scale: 1, // 按比例增加分辨率 }).then(async (canvas) => { //type==true横向 var pdfType = type ? "l" : "p"; var pdf = new JsPDF(pdfType, "mm", "a4"); // A4纸,纵向 var ctx = canvas.getContext("2d"); var a4w = type ? 277 : 190; var a4h = type ? 190 : 277; // A4大小,210mm x 297mm,四边各保留10mm的边距,显示区域190x277 var imgHeight = Math.floor((a4h * canvas.width) / a4w); // 按A4显示比例换算一页图像的像素高度 var renderedHeight = 0; while (renderedHeight < canvas.height) { var page = document.createElement("canvas"); page.width = canvas.width; page.height = Math.min(imgHeight, canvas.height - renderedHeight); // 可能内容不足一页 // 用getImageData剪裁指定区域,并画到前面创建的canvas对象中 page .getContext("2d") .putImageData( ctx.getImageData( 0, renderedHeight, canvas.width, Math.min(imgHeight, canvas.height - renderedHeight) ), 0, 0 ); pdf.addImage( page.toDataURL("image/jpeg", 1.0), "JPEG", 10, 10, a4w, Math.min(a4h, (a4w * page.height) / page.width) ); // 添加图像到页面,保留10mm边距 renderedHeight += imgHeight; if (renderedHeight < canvas.height) { pdf.addPage(); // 如果后面还有内容,添加一个空页 } // delete page; } // 保存文件 if (isDownLoad) { pdf.save(title + ".pdf"); resolve(); } else { // var pdfData = pdf.output('datauristring'); //获取base64Pdf const pdfBlob = pdf.output("blob"); //获取blob console.log(pdfBlob) //const pdfBlob = base64ToBlob(pdfData.split(',')[1], 'application/pdf'); const pdfFile = blobToFile(pdfBlob, title + ".pdf"); const FD = new FormData(); const nowDate = moment(new Date()).format("YYYYMMDDHHmmss"); FD.append("file", pdfFile, nowDate + title + ".pdf"); await uploadFile(null, FD).then((res) => { const result = res.result; uploadUrl = result.url; resolve(uploadUrl); }); } document.getElementsByClassName("divRemove")[0]?.remove(); document.querySelector(dom).style.height = divParentHeight + "px"; //利用promise 回调到页面中去 }); }); // type传有效值pdf则为横版 }; // pdf截断需要一个空白位置来补充 const getFooterElement = (remainingHeight, fillingHeight = 0) => { const newNode = document.createElement("div"); newNode.style.background = "#ffffff"; newNode.style.width = "calc(100% + 8px)"; newNode.style.marginLeft = "-4px"; newNode.style.marginBottom = "0px"; newNode.classList.add("divRemove"); newNode.style.height = remainingHeight + fillingHeight + "px"; return newNode; }; const isSplit = (nodes, index, pageHeight) => { // 判断是不是tr 如果不是高度存起来 // 表格里的内容要特殊处理 // tr.offsetTop 是tr到table表格的高度 // 所以计算高速时候要把表格外的高度加起来 // 生成的pdf没有表格了这里可以不做处理 直接计算就行 if (nodes[index].localName !== "tr") { //判断元素是不是tr noTableHeight += nodes[index].clientHeight; } if (nodes[index].localName !== "tr") { return ( nodes[index].offsetTop + nodes[index].offsetHeight < pageHeight && nodes[index + 1] && nodes[index + 1].offsetTop + nodes[index + 1].offsetHeight > pageHeight ); } else { return ( nodes[index].offsetTop + nodes[index].offsetHeight + noTableHeight < pageHeight && nodes[index + 1] && nodes[index + 1].offsetTop + nodes[index + 1].offsetHeight + noTableHeight > pageHeight ); } }; export function base64ToBlob(base64, type) { let binary = atob(base64); let array = []; for (let i = 0; i < binary.length; i++) { array.push(binary.charCodeAt(i)); } return new Blob([new Uint8Array(array)], { type }); } export function blobToFile(theBlob, fileName) { theBlob.lastModifiedDate = new Date(); theBlob.name = fileName; return theBlob; } export const uploadFile = async ( uploadUrl, params, module = "CA", fileSymbol = "", customFileName = "", renameFlag = true ) => { return await new Promise((resolve) => { const api = uploadUrl || window?.uploadOptions?.upload?.action axios .post( `${api}?module=${module}&fileSymbol=${fileSymbol}&customFileName=${customFileName}&renameFlag=${renameFlag}`, params, { headers: { "Content-Type": "multipart/form-data", }, } ) .then((res) => { resolve(res.data); }); }); }; /* */
原理: 先用 html2canvas
将 HTML 内容转换为图片,然后将图片放入 jsPDF
中生成 PDF。
优点
- 纯前端完成,不依赖服务器。
- 可以实现一键下载。
缺点
- 质量堪忧:生成的是图片型 PDF,文字无法选中、搜索,清晰度不高(尤其对高分辨率大页面)。
- 性能差,转换大页面容易卡顿甚至崩溃。
- 分页控制非常麻烦。
适用场景: 对PDF质量要求不高、内容量很小的场景,或者需要生成“快照”式PDF。
方案二:html2canvas + pdf-lib
代码
import html2Canvas from "html2canvas"; import { PDFDocument, rgb } from 'pdf-lib'; import moment from "moment"; import axios from "axios"; var noTableHeight = 0; //table外的元素高度 export const htmlPdf = async (title, dom, fileList, type, isDownLoad) => { return new Promise(async (resolve) => { const html = document.querySelector(dom); let htmlHeight = 0; const divParentHeight = JSON.parse( JSON.stringify(document.querySelector(dom).offsetHeight) ); let uploadUrl = ""; if (fileList) { for (let i = 0; i < fileList.length; i++) { htmlHeight = htmlHeight + fileList[i].offsetHeight + 30; } html.style.height = htmlHeight + "px"; const pageHeight = Math.floor((277 * html.scrollWidth) / 190) + 20; //计算pdf高度 for (let i = 0; i < fileList.length; i++) { //循环获取的元素 const multiple = Math.ceil( (fileList[i].offsetTop + fileList[i].offsetHeight) / pageHeight ); //元素的高度 if (isSplit(fileList, i, multiple * pageHeight)) { //计算是否超出一页 var _H = ""; //向pdf插入空白块的内容高度 if (fileList[i].localName !== "tr") { //判断是不是表格里的内容 _H = multiple * pageHeight - (fileList[i].offsetTop + fileList[i].offsetHeight); } else { _H = multiple * pageHeight - (fileList[i].offsetTop + fileList[i].offsetHeight + noTableHeight) + 20; } var newNode = getFooterElement(_H); //向pdf插入空白块的内容 const divParent = fileList[i].parentNode; // 获取该div的父节点 const next = fileList[i].nextSibling; // 获取div的下一个兄弟节点 const isTruncate = true; // 判断兄弟节点是否存在 if (next && !isTruncate) { // 存在则将新节点插入到div的下一个兄弟节点之前,即div之后 divParent.insertBefore(newNode, next); document.querySelector(dom).style.height = newNode.offsetHeight + divParentHeight + "px"; } else { // 否则向节点添加最后一个子节点 divParent.appendChild(newNode); } } } } if (!html) { throw new Error('DOM元素未找到'); } // 1. 分块处理大文档 const chunks = await splitContentIntoChunks(html); // 2. 创建PDF文档 const pdfDoc = await PDFDocument.create(); // 3. 处理每个分块 const reusableCanvas = document.createElement('canvas'); const ctx = reusableCanvas.getContext('2d'); // for (const chunk of chunks) { for (let i = 0; i < chunks.length; i++) { const chunk = chunks[i]; console.log(chunk) reusableCanvas.width = html.offsetWidth * 1 // type ? 277 : 190//window.innerWidth * 0.5; reusableCanvas.height = chunk.height; // 使用 html2Canvas 渲染到现有 Canvas await html2Canvas(chunk.html, { allowTaint: false, logging: false, useCORS: true, scale: 1, y: chunk.offset, windowHeight: chunk.offset, canvas: reusableCanvas, // 指定使用现有 Canvas }); // 4. 将canvas添加到PDF await addCanvasToPdf(pdfDoc, reusableCanvas); // 不需要手动释放内存,因为我们会复用同一个 Canvas // 只需清除上一块的内容 ctx.clearRect(0, 0, reusableCanvas.width, reusableCanvas.height); // 释放内存 reusableCanvas.width = 1; reusableCanvas.height = 1; // } } // 5. 保存或上传PDF const pdfBytes = await pdfDoc.save(); if (isDownLoad) { downloadPDF(pdfBytes, title); return; } else { const blob = new Blob([pdfBytes], { type: 'application/pdf' }); const pdfFile = blobToFile(blob, title + ".pdf"); const FD = new FormData(); const nowDate = moment(new Date()).format("YYYYMMDDHHmmss"); FD.append("file", pdfFile, nowDate + title + ".pdf"); await uploadFile(null, FD).then((res) => { const result = res.result; uploadUrl = result.url; resolve(uploadUrl); }); } }) } // pdf截断需要一个空白位置来补充 const getFooterElement = (remainingHeight, fillingHeight = 0) => { const newNode = document.createElement("div"); newNode.style.background = "#ffffff"; newNode.style.width = "calc(100% + 8px)"; newNode.style.marginLeft = "-4px"; newNode.style.marginBottom = "0px"; newNode.classList.add("divRemove"); newNode.style.height = remainingHeight + fillingHeight + "px"; return newNode; }; const isSplit = (nodes, index, pageHeight) => { // 判断是不是tr 如果不是高度存起来 // 表格里的内容要特殊处理 // tr.offsetTop 是tr到table表格的高度 // 所以计算高速时候要把表格外的高度加起来 // 生成的pdf没有表格了这里可以不做处理 直接计算就行 if (nodes[index].localName !== "tr") { //判断元素是不是tr noTableHeight += nodes[index].clientHeight; } if (nodes[index].localName !== "tr") { return ( nodes[index].offsetTop + nodes[index].offsetHeight < pageHeight && nodes[index + 1] && nodes[index + 1].offsetTop + nodes[index + 1].offsetHeight > pageHeight ); } else { return ( nodes[index].offsetTop + nodes[index].offsetHeight + noTableHeight < pageHeight && nodes[index + 1] && nodes[index + 1].offsetTop + nodes[index + 1].offsetHeight + noTableHeight > pageHeight ); } }; export function base64ToBlob(base64, type) { let binary = atob(base64); let array = []; for (let i = 0; i < binary.length; i++) { array.push(binary.charCodeAt(i)); } return new Blob([new Uint8Array(array)], { type }); } export function blobToFile(theBlob, fileName) { theBlob.lastModifiedDate = new Date(); theBlob.name = fileName; return theBlob; } /** * 将大内容分割成多个可处理的部分 */ async function splitContentIntoChunks(html) { const chunks = []; const totalHeight = html.scrollHeight; const viewportHeight = window.innerHeight; const chunkHeight = Math.min(viewportHeight, 960) * 1.5; // 每块最多5000px for (let offset = 0; offset < totalHeight; offset += chunkHeight) { const height = Math.min(chunkHeight, totalHeight - offset); chunks.push({ html, height, offset }); // 避免浏览器卡顿 await new Promise(resolve => setTimeout(resolve, 10)); } return chunks; } /** * 将canvas添加到PDF文档 */ async function addCanvasToPdf(pdfDoc, canvas) { // 转换为PNG并嵌入 const pngImage = await pdfDoc.embedPng(canvas.toDataURL('image/png')); // const page = pdfDoc.addPage([canvas.width, canvas.height]); // A4尺寸 console.log(pngImage) // 计算缩放比例以适应页面 let scale = Math.min( (page.getWidth() - 40) / pngImage.width, // 左右各20pt边距 (page.getHeight() - 40) / pngImage.height // 上下各20pt边距 ); page.drawImage(pngImage, { x: 20, y: 20, width: pngImage.width * scale, height: pngImage.height * scale, }); } /** * 下载PDF文件 */ function downloadPDF(pdfBytes, fileName) { const blob = new Blob([pdfBytes], { type: 'application/pdf' }); const link = document.createElement('a'); link.href = URL.createObjectURL(blob); link.download = `${fileName}.pdf`; document.body.appendChild(link); link.click(); document.body.removeChild(link); URL.revokeObjectURL(link.href); } export const uploadFile = async ( uploadUrl, params, module = "CA", fileSymbol = "", customFileName = "", renameFlag = true ) => { return await new Promise((resolve) => { const api = uploadUrl || window?.uploadOptions?.upload?.action axios .post( `${api}?module=${module}&fileSymbol=${fileSymbol}&customFileName=${customFileName}&renameFlag=${renameFlag}`, params, { headers: { "Content-Type": "multipart/form-data", }, } ) .then((res) => { resolve(res.data); }); }); };
原理: 用方案一,一旦遇到超长内容打印的时候就会出现文档内容全部是黑屏现象,原因是因为canvas的画布高度是有限的,方案二使用分块的形式,对内容进行分割得到多个canvas,然后逐一添加到pdf,可以避免黑屏问题,
优点
- 纯前端完成,不依赖服务器。
- 可以打印复杂量多内容。
缺点
- 质量堪忧:生成的是图片型 PDF,文字无法选中、搜索,清晰度不高(尤其对高分辨率大页面)。
- 性能差,转换超长内容时用时很久。
使用时注意事项
- 注意如果是转换弹框中modal中的内容,一定要另外起一个新页面,避免内容中太多静态资源,因为canvas转换成base64图片的时候会一直加载容器底下的所有静态资源。
- 想要控制一页展示内容多少,用方法
splitContentIntoChunk
s中的chunkHeight
就行,参数值越大,一页展示的内容越多,生成的canvas越少,会节省时间,但是打印出来的内容会很小,所以要调整好自己合适的尺寸。 - 将canvas添加到PDF文档的方法addCanvasToPdf使用时
const page = pdfDoc.addPage([canvas.width, canvas.height]); // A4尺寸
这行代码里面的image的宽高就直接用canvas的,不要在自定义,不然容易出现变形
适用场景: 对PDF质量要求不高、内容量很小的场景,或者需要生成“快照”式PDF。
方案三:window.print()
如果只是想单纯的得到一份pdf直接右击另存为pdf格式的就能实现
方案四:提供静态模版给服务端生成
这个方案是最好的,就是费服务端,需要前后端配合,不论是性能还是得到的pdf清晰度都是比较好的。
到此这篇关于vue3实现html转成pdf并导出的示例代码的文章就介绍到这了,更多相关vue3实现html转pdf内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!