在项目中遇到一个根据页面导出PDF的功能
在查阅了网上论坛发现大部分都是使用1 2的这两种方案
我使用的是modern-screenshot 截图 + jspdf 完美的解决了分页截断的问题
分页截断在此借鉴了某金上一位大佬的文章,大家可转自查看jsPDF + html2canvas A4分页截断 完美解决方案(含代码 + 案例) - 掘金
1、html2pdf.js 库
2、html2canvas + jspdf(最常见)
3、window.print() 浏览器自带的打印
4、modern-screenshot + jspdf(本人使用)
前两个方法导出的时候会出现一些问题 下载预览的效果和实际html页面差距较大
比如html2canvas如果说版本号不为1.0.0文字会下移如图
即使使用了更换了版本号也同样会出现dom结构不对,各种各样未知的问题
废话不多说直接开始使用步骤
// 代码步骤就两步 // 第一步下包 yarn add jspdf yarn add modern-screenshot // 第二步使用 async function downloadPDF() { const element = document.querySelector('.tabsStyle'); const header = document.querySelector('.pdf-header'); const footer = document.querySelector('.pdf-footer'); try { downloading.value = true; await outputPDF2({ element: element, footer: footer, header: header, contentWidth: 550, filname:'测试A4分页.pdf' }); } catch (error) { } finally { downloading.value = false; } }
复制
直接附上js代码,可直接复制!
希望对大家有帮助,也欢迎大家积极讨论并指出我的不足
import jsPDF from 'jspdf'; import { domToCanvas } from 'modern-screenshot'; const A4_WIDTH = 592.28; const A4_HEIGHT = 841.89; // 将元素转化为canvas元素 // 通过 放大 提高清晰度 // width为内容宽度 async function toCanvas(element, width) { // canvas元素 const canvas = await domToCanvas(element, { scale: 2, backgroundColor: '#fff', }); console.log(canvas, 'canvas'); // 获取canavs转化后的宽度 const canvasWidth = canvas.width; // 获取canvas转化后的高度 const canvasHeight = canvas.height; // 高度转化为PDF的高度 const height = (width / canvasWidth) * canvasHeight; // 转化成图片Data const canvasData = canvas.toDataURL('image/jpeg', 1.0); //console.log(canvasData) return { width, height, data: canvasData }; } /** * 生成pdf(A4多页pdf截断问题, 包括页眉、页脚 和 上下左右留空的护理) * @param {Object} param * @param {HTMLElement} param.element - 需要转换的dom根节点 * @param {number} [param.contentWidth=550] - 一页pdf的内容宽度,0-592.28 * @param {string} [param.filename='document.pdf'] - pdf文件名 * @param {HTMLElement} param.header - 页眉dom元素 * @param {HTMLElement} param.footer - 页脚dom元素 */ export async function outputPDF2({ element, contentWidth = 550, footer, header, filename = '测试A4分页.pdf', }) { if (!(element instanceof HTMLElement)) { return; } // jsPDFs实例 const pdf = new jsPDF({ unit: 'pt', format: 'a4', orientation: 'p', }); // 一页的高度, 转换宽度为一页元素的宽度 const { width, height, data } = await toCanvas(element, contentWidth); // 添加页脚 async function addHeader(header, pdf, contentWidth) { const { height: headerHeight, data: headerData, width: hWidth, } = await toCanvas(header, contentWidth); pdf.addImage(headerData, 'JPEG', 0, 0, contentWidth, headerHeight); } // 添加页眉 async function addFooter(pageNum, now, footer, pdf, contentWidth) { const newFooter = footer.cloneNode(true); newFooter.querySelector('.pdf-footer-page').innerText = now; newFooter.querySelector('.pdf-footer-page-count').innerText = pageNum; document.documentElement.append(newFooter); const { height: footerHeight, data: footerData, width: fWidth, } = await toCanvas(newFooter, contentWidth); pdf.addImage(footerData, 'JPEG', 0, A4_HEIGHT - footerHeight, contentWidth, footerHeight); } // 添加 function addImage(_x, _y, pdf, data, width, height) { pdf.addImage(data, 'JPEG', _x, _y, width, height); } // 增加空白遮挡 function addBlank(x, y, width, height, pdf) { pdf.setFillColor(255, 255, 255); pdf.rect(x, y, Math.ceil(width), Math.ceil(height), 'F'); } // 页脚元素 经过转换后在PDF页面的高度 const { height: tfooterHeight } = await toCanvas(footer, contentWidth); // 页眉元素 经过转换后在PDF的高度 const { height: theaderHeight } = await toCanvas(header, contentWidth); // 距离PDF左边的距离,/ 2 表示居中 const baseX = (A4_WIDTH - contentWidth) / 2; // 预留空间给左边 // 距离PDF 页眉和页脚的间距, 留白留空 const baseY = 15; // 出去页头、页眉、还有内容与两者之间的间距后 每页内容的实际高度 const originalPageHeight = A4_HEIGHT - tfooterHeight - theaderHeight - 2 * baseY; // 元素在网页页面的宽度 const elementWidth = element.offsetWidth; // PDF内容宽度 和 在HTML中宽度 的比, 用于将 元素在网页的高度 转化为 PDF内容内的高度, 将 元素距离网页顶部的高度 转化为 距离Canvas顶部的高度 const rate = contentWidth / elementWidth; // 每一页的分页坐标, PDF高度, 初始值为根元素距离顶部的距离 const pages = [rate * getElementTop(element)]; // 获取元素距离网页顶部的距离 // 通过遍历offsetParant获取距离顶端元素的高度值 function getElementTop(element) { let actualTop = element.offsetTop; let current = element.offsetParent; while (current && current !== null) { actualTop += current.offsetTop; current = current.offsetParent; } return actualTop; } // 遍历正常的元素节点 // function traversingNodes(nodes) { for (let i = 0; i < nodes.length; ++i) { const one = nodes[i]; // 需要判断跨页且内部存在跨页的元素 const isDivideInside = one.classList && one.classList.contains('tabsStyle'); // 变量为下载的大容器 // 图片元素不需要继续深入,作为深度终点 const isIMG = one.tagName === 'IMG'; // 深度终点 判断该元素是否进行分页判断 const isAntCard = one.classList && one.classList.contains('ant-card'); //该dom是否需要进行分页判断 // 特殊的富文本元素 const isEditor = one.classList && one.classList.contains('editor'); // 对需要处理分页的元素,计算是否跨界,若跨界,则直接将顶部位置作为分页位置,进行分页,且子元素不需要再进行判断 let { offsetHeight } = one; // 计算出最终高度 let offsetTop = getElementTop(one); // dom转换后距离顶部的高度 // 转换成canvas高度 const top = rate * offsetTop; // 对于需要进行分页且内部存在需要分页(即不属于深度终点)的元素进行处理 if (isDivideInside) { // 执行位置更新操作 updatePos(rate * offsetHeight, top, one); // 执行深度遍历操作 traversingNodes(one.childNodes); } // 对于深度终点元素进行处理 else if (isAntCard || isIMG) { // dom高度转换成生成pdf的实际高度 // 代码不考虑dom定位、边距、边框等因素,需在dom里自行考虑,如将box-sizing设置为border-box updatePos(rate * offsetHeight, top, one); } else if (isEditor) { // 执行位置更新操作 updatePos(rate * offsetHeight, top, one); // 遍历富文本节点 traversingEditor(one.childNodes); } // 对于普通元素,则判断是否高度超过分页值,并且深入 else { // 执行位置更新操作 updateNomalElPos(top); // 遍历子节点 traversingNodes(one.childNodes); } } return; } // 对于富文本元素,观察所得段落之间都是以<p> / <img> 元素相隔,因此不需要进行深度遍历 (仅针对个人遇到的情况) function traversingEditor(nodes) { // 遍历子节点 for (let i = 0; i < nodes.length; ++i) { const one = nodes[i]; let { offsetHeight } = one; let offsetTop = getElementTop(one); const top = (contentWidth / elementWidth) * offsetTop; updatePos((contentWidth / elementWidth) * offsetHeight, top, one); } } // 普通元素更新位置的方法 // 普通元素只需要考虑到是否到达了分页点,即当前距离顶部高度 - 上一个分页点的高度 大于 正常一页的高度,则需要载入分页点 function updateNomalElPos(top) { if (top - (pages.length > 0 ? pages[pages.length - 1] : 0) > originalPageHeight) { pages.push((pages.length > 0 ? pages[pages.length - 1] : 0) + originalPageHeight); } } // 可能跨页元素位置更新的方法 // 需要考虑分页元素,则需要考虑两种情况 // 1. 普通达顶情况,如上 // 2. 当前距离顶部高度加上元素自身高度 大于 整页高度,则需要载入一个分页点 function updatePos(eheight, top) { // 如果高度已经超过当前页,则证明可以分页了 if (top - (pages.length > 0 ? pages[pages.length - 1] : 0) >= originalPageHeight) { pages.push((pages.length > 0 ? pages[pages.length - 1] : 0) + originalPageHeight); } // 若 距离当前页顶部的高度 加上元素自身的高度 大于 一页内容的高度, 则证明元素跨页,将当前高度作为分页位置 else if ( top + eheight - (pages.length > 0 ? pages[pages.length - 1] : 0) > originalPageHeight && top != (pages.length > 0 ? pages[pages.length - 1] : 0) ) { pages.push(top - 25); //分页判断 } } // 深度遍历节点的方法 traversingNodes(element.childNodes); // 可能会存在遍历到底部元素为深度节点,可能存在最后一页位置未截取到的情况 if (pages[pages.length - 1] + originalPageHeight < height) { pages.push(pages[pages.length - 1] + originalPageHeight); } //console.log({ pages, contentWidth, width,height }) // 根据分页位置 开始分页 for (let i = 0; i < pages.length; ++i) { // 根据分页位置新增图片 addImage(baseX, baseY + theaderHeight - pages[i], pdf, data, width, height); // 将 内容 与 页眉之间留空留白的部分进行遮白处理 addBlank(0, theaderHeight, A4_WIDTH, baseY, pdf); // 将 内容 与 页脚之间留空留白的部分进行遮白处理 addBlank(0, A4_HEIGHT - baseY - tfooterHeight, A4_WIDTH, baseY, pdf); // 对于除最后一页外,对 内容 的多余部分进行遮白处理 if (i < pages.length - 1) { // 获取当前页面需要的内容部分高度 const imageHeight = pages[i + 1] - pages[i]; // 对多余的内容部分进行遮白 addBlank(0, baseY + imageHeight + theaderHeight, A4_WIDTH, A4_HEIGHT - imageHeight, pdf); } // 添加页眉 await addHeader(header, pdf, A4_WIDTH); // 添加页脚 await addFooter(pages.length, i + 1, footer, pdf, A4_WIDTH); // 若不是最后一页,则分页 if (i !== pages.length - 1) { // 增加分页 pdf.addPage(); } } return pdf.save(filename); }
复制