在之前的招聘系统中,有一个信息展示页,其中包含了候选人的详细信息,是通过前端渲染字段来进行展示。信息量比较大,在页面的局部容器中滚动,并且支持编辑、删除等操作。为了便于候选人信息的存档和分享,我们需要实现将这些信息导出为PDF的功能。导出方案对比
后端导出方案
后端导出的工作原理:
- 前端发送导出请求,携带需要导出的数据标识
- 后端接收请求,从数据库获取完整数据
- 后端使用专业的PDF生成库(如iText、wkhtmltopdf等)生成PDF文件
- 后端将生成的PDF文件提供下载链接或直接返回文件流
后端导出的优势:
- 生成的PDF格式更规范,排版控制更精确
- 不占用前端资源,大数据量处理更稳定
- 服务器端可以进行更复杂的业务逻辑处理
后端导出的劣势:
- 需要额外的服务器资源开销
- 前后端交互增加,实现复杂度较高
- 无法完全复用前端已有的UI样式
前端导出方案
前端导出的工作原理:
- 直接在浏览器端捕获DOM元素
- 使用canvas技术将DOM转换为图像
- 将图像数据转换为PDF文件
- 触发浏览器下载功能
前端导出的优势:
- 实现简单,无需后端额外开发
- 可以完全保留前端的样式和布局
- 即时生成,无需等待服务器响应
- 减轻服务器压力
前端导出的劣势:
- 大量DOM元素处理可能导致浏览器性能问题
- 不同浏览器兼容性可能存在差异
- 跨域资源处理需要额外配置
综合考虑项目需求和实现复杂度,我们选择了前端导出方案。下面将详细介绍实现过程。
前端导出实现过程
1. 基础导出功能实现
首先,我们需要引入相关的库来实现PDF导出功能。我们使用了以下两个核心库:
html2canvas: 将DOM元素转换为canvas图像jspdf: 将canvas图像转换为PDF文件
基础实现代码:
exportToPdf(fileName) { const pdfFileName = fileName || ('简历_' + this.getNowFormatDate() + '.pdf'); var element = document.getElementById('DomPdf') var w = element.offsetWidth var h = element.offsetHeight var offsetTop = element.offsetTop + 30 var offsetLeft = element.offsetLeft var canvas = document.createElement('canvas') var abs = 0 var win_i = document.body.clientWidth var win_o = window.innerWidth if (win_o > win_i) { abs = (win_o - win_i) / 2 } canvas.width = w * 2 canvas.height = h * 2 var context = canvas.getContext('2d') context.scale(2, 2) context.translate(-offsetLeft - abs, -offsetTop) html2Canvas(element, { allowTaint: true, useCORS: true, scale: 2, }).then((canvas) => { var contentWidth = canvas.width var contentHeight = canvas.height var imgWidth = 595.28 var imgHeight = (592.28 / contentWidth) * contentHeight var pageHeight = 841.89; var position = 50; var pdf = new JsPDF('', 'pt', 'a4') pdf.addImage(canvas.toDataURL('image/jpeg', 1.0), 'JPEG', 0, position, imgWidth, imgHeight) pdf.save(pdfFileName) })}
此时可以看到,已经实现导出功能,但是展示的还有一些问题。比如左侧文本太贴近边缘,内容被分页展示,需要做调整。
2. 样式调整优化
最初实现后,我们发现导出的PDF样式不够美观,主要存在以下问题:
- 内容与页面边缘过于贴近
- 长内容可能会被截断或显示不完整
- 某些情况下会出现黑色背景块
我们对代码进行了以下优化:
const leftPadding = 20 const adjustedImgWidth = imgWidth - leftPadding if (contentHeight < pageHeight) { pdf.setFillColor(255, 255, 255) pdf.rect(0, 0, imgWidth, pageHeight, 'F') var pageData = canvas.toDataURL('image/jpeg', 1.0) pdf.addImage( pageData, 'JPEG', leftPadding, position, adjustedImgWidth, imgHeight )} else { var pageHeightInCanvas = (pageHeight * contentWidth) / 592.28 var pageCount = Math.ceil(contentHeight / pageHeightInCanvas) for (var i = 0; i < pageCount; i++) { if (i > 0) pdf.addPage() var newCanvas = document.createElement('canvas') newCanvas.width = contentWidth newCanvas.height = Math.min( pageHeightInCanvas, contentHeight - i * pageHeightInCanvas ) var ctx = newCanvas.getContext('2d') ctx.fillStyle = '#ffffff' ctx.fillRect(0, 0, newCanvas.width, newCanvas.height) ctx.drawImage( canvas, 0, i * pageHeightInCanvas, contentWidth, newCanvas.height ) var newPageData = newCanvas.toDataURL('image/jpeg', 1.0) var currentPageHeight = (592.28 / contentWidth) * newCanvas.height pdf.setFillColor(255, 255, 255) pdf.rect(0, 0, imgWidth, pageHeight, 'F') pdf.addImage( newPageData, 'JPEG', leftPadding, position, adjustedImgWidth, currentPageHeight ) }}
优化后的效果:
3. 排除不需要导出的按钮和元素
在导出过程中,我们发现页面中的编辑按钮、删除按钮、下载按钮等操作按钮也被一并导出到PDF中,这不是我们希望的。我们需要在导出时排除这些不必要的元素。
排除按钮和元素的实现:
html2Canvas(element, { allowTaint: true, useCORS: true, scale: 2, ignoreElements: function (el) { try { const hasClass = (element, className) => { if (!element || !element.classList) return false try { return element.classList.contains(className) } catch (e) { return false } } const getElementTagName = (element) => { if (!element || !element.tagName) return '' try { return element.tagName.toLowerCase() } catch (e) { return '' } } const tagName = getElementTagName(el) const getElementText = (element) => { if (!element) return '' try { return (element.textContent || element.innerText || '').replace( /\s+/g, '' ) } catch (e) { return '' } } const isButton = tagName === 'a-button' || hasClass(el, 'ant-btn') || tagName === 'button' const elementText = getElementText(el) const isDownloadButton = isButton && (elementText.includes('下载简历') || elementText.includes('下载')) const isActionButton = isButton && (elementText.includes('编辑') || elementText.includes('删除') || elementText.includes('新增')) const isPagination = hasClass(el, 'ant-pagination') const isApprovePeople = el.id === 'approvePeople' const isJBtnsContainer = hasClass(el, 'jBtns') const shouldIgnore = isDownloadButton || isActionButton || isJBtnsContainer || isPagination || isApprovePeople return shouldIgnore } catch (e) { console.error('Error in ignoreElements:', e) return false } },})
排除按钮后的效果对比:
4. 使用Promise优化导出状态控制
在实际使用中,我们发现需要在导出前和导出完成后执行一些特定操作(例如显示加载状态、临时显示完整的手机号和身份证信息等)。为了更精确地控制导出状态,我们将导出方法改造为返回Promise。
Promise优化实现:
exportToPdf(fileName) { return new Promise((resolve, reject) => { html2Canvas(element, { }).then(async (canvas) => { pdf.save(pdfFileName); this.showPrintSuccessMessage = true; setTimeout(() => { this.showPrintSuccessMessage = false; }, 3000); resolve(); }).catch(error => { console.error('PDF导出失败:', error); reject(error); }); });}
外部组件使用示例:
handleExport() { this.isExporting = true; this.exportToPdf().then(() => { this.isExporting = false; }).catch(() => { this.isExporting = false; });}
注意事项
(1) 跨域资源处理:
- 确保设置了
allowTaint:true和useCORS:true,否则可能导致跨域图片无法正常显示 - 服务器端需要配置正确的CORS策略
(2) 性能优化:
- 对于包含大量DOM元素的页面,导出过程可能会比较耗时,建议添加加载提示
- 可以适当降低
scale值来减小生成的文件大小,但会影响图片质量
(3) 元素排除规则:
ignoreElements函数需要根据实际项目中的按钮和容器类名进行调整- 确保排除逻辑足够健壮,避免误删需要显示的内容
(4) 样式保持:
- 某些CSS属性在转换为canvas时可能无法完全保留,需要进行兼容性测试
- 对于复杂的布局,可能需要为导出模式单独设计样式
(5) 移动端兼容性:
- 在移动设备上导出大型PDF可能会遇到内存限制问题
- 建议在移动设备上提供替代方案或限制导出内容
(6) 导出状态管理:
- 使用Promise可以更精确地控制导出前后的操作
- 确保在任何情况下(成功或失败)都能正确恢复状态
(7) 浏览器兼容性:
- 不同浏览器对canvas和PDF生成的支持程度可能有所不同
- 建议在主要目标浏览器上进行充分测试
通过以上实现和注意事项,我们可以为用户提供一个稳定、高效且美观的pdf导出功能,满足目前的业务需求。
完整的导出代码
htmlToPdf-minxi.js 文件
import html2Canvas from'html2canvas'importJsPDFfrom'jspdf'import printJS from'print-js'exportconst jPdfMixin = { methods: { exportToPdf(fileName) { returnnewPromise((resolve, reject) => { const pdfFileName = fileName || '简历_' + this.getNowFormatDate() + '.pdf' var element = document.getElementById('DomPdf') var w = element.offsetWidth var h = element.offsetHeight var offsetTop = element.offsetTop + 30 var offsetLeft = element.offsetLeft var canvas = document.createElement('canvas') var abs = 0 var win_i = document.body.clientWidth var win_o = window.innerWidth if (win_o > win_i) { abs = (win_o - win_i) / 2 } canvas.width = w * 2 canvas.height = h * 2 var context = canvas.getContext('2d') context.scale(2, 2) context.translate(-offsetLeft - abs, -offsetTop) html2Canvas(element, { allowTaint: true, useCORS: true, scale: 2, ignoreElements: function (el) { try { consthasClass = (element, className) => { if (!element || !element.classList) returnfalse try { return element.classList.contains(className) } catch (e) { returnfalse } } constgetElementTagName = (element) => { if (!element || !element.tagName) return'' try { return element.tagName.toLowerCase() } catch (e) { return'' } } const tagName = getElementTagName(el) constgetElementText = (element) => { if (!element) return'' try { return ( element.textContent || element.innerText || '' ).replace(/\s+/g, '') } catch (e) { return'' } } const isButton = tagName === 'a-button' || hasClass(el, 'ant-btn') || tagName === 'button' const elementText = getElementText(el) const isDownloadButton = isButton && (elementText.includes('下载简历') || elementText.includes('下载')) const isActionButton = isButton && (elementText.includes('编辑') || elementText.includes('删除') || elementText.includes('新增')) const isPagination = hasClass(el, 'ant-pagination') const isApprovePeople = el.id === 'approvePeople' const isJBtnsContainer = hasClass(el, 'jBtns') const shouldIgnore = isDownloadButton || isActionButton || isJBtnsContainer || isPagination || isApprovePeople return shouldIgnore } catch (e) { console.error('Error in ignoreElements:', e) returnfalse } }, }) .then(async (canvas) => { var contentWidth = canvas.width var contentHeight = canvas.height var imgWidth = 595.28 var imgHeight = (592.28 / contentWidth) * contentHeight var pageHeight = 841.89 var position = 50 var pdf = newJsPDF('', 'pt', 'a4') const leftPadding = 20 const adjustedImgWidth = imgWidth - leftPadding if (contentHeight < pageHeight) { pdf.setFillColor(255, 255, 255) pdf.rect(0, 0, imgWidth, pageHeight, 'F') var pageData = canvas.toDataURL('image/jpeg', 1.0) pdf.addImage( pageData, 'JPEG', leftPadding, position, adjustedImgWidth, imgHeight ) } else { var pageHeightInCanvas = (pageHeight * contentWidth) / 592.28 var pageCount = Math.ceil(contentHeight / pageHeightInCanvas) for (var i = 0; i < pageCount; i++) { if (i > 0) pdf.addPage() var newCanvas = document.createElement('canvas') newCanvas.width = contentWidth newCanvas.height = Math.min( pageHeightInCanvas, contentHeight - i * pageHeightInCanvas ) var ctx = newCanvas.getContext('2d') ctx.fillStyle = '#ffffff' ctx.fillRect(0, 0, newCanvas.width, newCanvas.height) ctx.drawImage( canvas, 0, i * pageHeightInCanvas, contentWidth, newCanvas.height ) var newPageData = newCanvas.toDataURL('image/jpeg', 1.0) var currentPageHeight = (592.28 / contentWidth) * newCanvas.height pdf.setFillColor(255, 255, 255) pdf.rect(0, 0, imgWidth, pageHeight, 'F') pdf.addImage( newPageData, 'JPEG', leftPadding, position, adjustedImgWidth, currentPageHeight ) } } pdf.save(pdfFileName) this.showPrintSuccessMessage = true setTimeout(() => { this.showPrintSuccessMessage = false }, 3000) resolve() }) .catch((error) => { console.error('PDF导出失败:', error) reject(error) }) }) }, printResume() { var element = document.getElementById('DomPdf') html2Canvas(element, { allowTaint: true, useCORS: true, scale: 2, }).then((canvas) => { const jsPdfBytes = canvas.toDataURL('image/jpeg', 1.0) printJS({ printable: jsPdfBytes, type: 'image', showModal: true }) }) }, getNowFormatDate() { var date = newDate() var seperator1 = '' var year = date.getFullYear() var month = date.getMonth() + 1 var strDate = date.getDate() var hour = date.getHours() var min = date.getMinutes() var second = date.getSeconds() if (month >= 1 && month <= 9) { month = '0' + month } if (strDate >= 0 && strDate <= 9) { strDate = '0' + strDate } if (hour >= 0 && hour <= 9) { hour = '0' + hour } if (min >= 0 && min <= 9) { min = '0' + min } if (second >= 0 && second <= 9) { second = '0' + second } var currentdate = year + seperator1 + month + seperator1 + strDate + seperator1 + hour + seperator1 + min + seperator1 + second return currentdate }, getPdf() { this.exportToPdf() }, },}
阅读原文:原文链接
该文章在 2025/12/11 8:52:53 编辑过