这次终于轮到前端给后端兜底了🤣
(??金石瓜分计划回归,速戳上图了解详情??)需求交代最近我们项目组开发了个互联网采集的功能,也就是后端合理抓取了第三方的文章,数据结构大致如下:
h1前端人/h1p学好前端,走遍天下都不怕/p数据抓取到后,存储到数据库,然后前端请求接口获取到数据,直接在页面预览
divv-html='articleContent'/div整个需求已经交代清楚
这个需求有点为难后端了前天,客户说要新增一个文章的 pdf 导出功能,但就是这么一个合情合理的需求,却把后端为难住了,原因是部分数据采集过来的结构可能是这样的:
h1前端人/h1p学好前端,走遍天下都不怕/pdiv前端强,前端狂,交互特效我称王!pJS 写得好,需求改不了!/ppReact Vue 两手抓,高薪 offer 到你家!pp浏览器里横着走, bug 见我都绕道!/ppChrome 调试一声笑, IE 泪洒旧时光!/pspanGit 提交不留情,版本回退我最行!仔细的人就能发现问题了,很多 html 元素存在没有完整的闭合情况
但浏览器是强大的,丝毫不影响渲染效果,原来浏览器自动帮我们补全结构了
可后端处理这件事就没那么简单了,爬取到的数据也比我举例的要复杂的多,使用第三方插件将 html 转 pdf 时会识别标签异常等问题,因此程序会抛异常
来自后端的建议苦逼的后端折腾了很久,还是没折腾出来,终于他发现前端页面有个右键打印的功能,也就是:
于是他说:浏览器这玩意整挺好啊,前端能不能研究研究,尝试从前端实现导出
那就研究研究我印象中,确实有个叫vue-print-nb的前端插件,可以实现这个功能
但....... 等等,这个插件仅仅是唤起打印的功能,我总不能真做成这样,让用户另存为 pdf 吧
于是,只能另辟蹊径,终于我找到了这么个仓库:github.com/burc-li/vue…[1]
里面实现了 dom 元素导出 pdf 的功能
效果很不错,技术用到了jspdf、html2canvas这两个第三方库,代码十分简单
constdownLoadPdfA4Single= () = {constpdfContaniner =document.querySelector('#pdfContaniner')html2canvas(pdfContaniner).then(canvas={ // 返回图片dataURL,参数:图片格式和清晰度(0-1) constpageData = canvas.toDataURL('image/jpeg',1.0)
// 方向纵向,尺寸ponits,纸张格式 a4 即 [595.28, 841.89] constA4Width =595.28 constA4Height =841.89// A4纸宽 constpageHeight = A4Height = A4Width * canvas.height/ canvas.width? A4Height : A4Width * canvas.height/ canvas.width constpdf =newjsPDF('portrait','pt', [A4Width, pageHeight])
// addImage后两个参数控制添加图片的尺寸,此处将页面高度按照a4纸宽高比列进行压缩 pdf.addImage( pageData, 'JPEG', 0, 0, A4Width, A4Width * canvas.height/ canvas.width, ) pdf.save('下载一页PDF(A4纸).pdf') })}技术流程大致就是:
dom - canvascanvas - imageimage - pdf似乎一切都将水到渠成了
困在眼前的难题这个技术栈,最核心的就是:必须要用到 dom 元素渲染
如果你尝试将打印的元素设置样式:
display: none;或
visibility: hidden;或
opacity:0;执行导出功能都将抛异常或者只能导出一个空白的 pdf
这时候有人会问了:为什么要设置 dom 元素为不可见?
试想一下,你做了一个导出功能,总不能让客户必须先打开页面等 html 渲染完后,再导出吧?
客户的理想状态是:在列表的操作列里,有个导出按钮,点击就可以导出 pdf 了
何况还需要实现批量勾选导出的功能,总不能程序控制,导出一个 pdf 就 open 一个窗口渲染 html 吧
寻找新方法此路不通,就只能重新寻找新的方向,不过也没费太多功夫,就找到了另外一个插件html2pdf.js解决了这事
这插件用起来也极其简单
npminstall html2pdf.jstemplatedivclass="container" button@click="generatePDF"下载PDF/button/div/templatescriptsetupimporthtml2pdffrom'html2pdf.js'
// 使用示例letelement =` 前端人/h1 学好前端,走遍天下都不怕 div前端强,前端狂,交互特效我称王! JS 写得好,需求改不了! React Vue 两手抓,高薪 offer 到你家! 浏览器里横着走, bug 见我都绕道!/p Chrome 调试一声笑, IE 泪洒旧时光! spanGit 提交不留情,版本回退我最行!`;
functiongeneratePDF() { // 配置选项 constopt = { margin: 10, filename: 'hello_world.pdf', image: {type:'jpeg',quality:0.98}, html2canvas: {scale:2}, jsPDF: {unit:'mm',format:'a4',orientation:'portrait'} // 生成PDF并导出 html2pdf().from(element).set(opt).save();}/script功能正常,似乎一切都完美
问题没有想的那么简单如果我们的html是纯文本元素,这程序跑起来没有任何问题,但我们抓取的信息都源于互联网,html结构怎么可能会这么简单?如果我们的html中包含图片信息,例如:
// 使用示例let element = `div imgsrc='http://t13.baidu.com/it/u=2041049195,1001882902&fm=224&app=112&f=JPEG?w=500&h=500'style="width: 300px;"/ p职业:前端/p p技能:唱、跳、rap/p/div`;此时你会发现,导出来的 pdf,图片占位处是个空白块
?思考一下:类似案例中的图片加载方式,都是 get 方式的异步请求,而异步请求就会导致图片还没渲染完成,但导出的程序已经执行完成情况(最直接的观察方式就是,把这个元素放到浏览器上渲染,会发现图片也是过一会才慢慢加载完成的)
不过我不确定html2pdf.js这个插件是否会发起图片请求,但不管发不发起,导出的行为明显是在图片渲染前完成的,就导致了这个空白块的存在
?问题分析完了,那就解决吧既然图片异步加载不行,那就使用图片同步加载吧
不是吧,你问我:什么是图片同步加载?我也不晓得,这个词是我自己当下凭感觉造的,如有雷同,纯属巧合了
那我理解的图片同步加载是什么意思呢?简单来说,就是将图片转成Base64,因为这种方式,即使说无网的情况也能正常加载图片,因此我凭感觉断定,这就是图片同步加载
基于这个思路,我写了个 demo
templatedivclass="container" button@click="generatePDF"下载PDF/button/div/templatescriptsetupimporthtml2pdffrom'html2pdf.js'
asyncfunctionconvertImagesToBase64(htmlString) {// 创建一个临时DOM元素来解析HTMLconsttempDiv =document.createElement('div'); tempDiv.innerHTML= htmlString;
// 获取所有图片元素constimages = tempDiv.querySelectorAll('img');
// 遍历每个图片并转换for(constimgofimages) { try{ constbase64 =awaitgetBase64FromUrl(img.src); img.src= base64; }catch(error) { console.error(`无法转换图片${img.src}:`, error); // 保留原始URL如果转换失败 } }
// 返回转换后的HTMLreturntempDiv.innerHTML;}
// 图片转base64functiongetBase64FromUrl(url) {returnnewPromise((resolve, reject) ={ constimg =newImage(); img.crossOrigin='Anonymous';// 处理跨域问题
img.onload=() ={ constcanvas =document.createElement('canvas'); canvas.width= img.width; canvas.height= img.height;
constctx = canvas.getContext('2d'); ctx.drawImage(img,0,0);
// 获取Base64数据 constdataURL = canvas.toDataURL('image/png'); resolve(dataURL);
img.onerror=() ={ reject(newError('图片加载失败'));
img.src= url;}
// 使用示例letelement =` div img src='http://t13.baidu.com/it/u=2041049195,1001882902&fm=224&app=112&f=JPEG?w=500&h=500' style="width: 300px;" / 职业:前端 技能:唱、跳、rap /div`;
functiongeneratePDF() {convertImagesToBase64(element) .then(convertedHtml={ // 配置选项 constopt = { margin: 10, filename: '前端大法好.pdf', image: {type:'jpeg',quality:0.98}, html2canvas: {scale:2}, jsPDF: {unit:'mm',format:'a4',orientation:'portrait'}
// 生成PDF并导出 html2pdf().from(convertedHtml).set(opt).save(); }) .catch(error={ console.error('转换过程中出错:', error);}/script此时就大功告成啦!不过得提一句:图片的 URL 链接必须是同源或者允许跨越的,否则就会存在图片加载异常的问题
修复图片过大的问题部分图片的宽度会过大,导致图片加载不全的问题,这在预览的情况下也存在
因为需要加上样式限定
img{max-width:100%;max-height:100%;vertical-align: middle;height: auto!important;width: auto!important;margin:10px0;}这样就正常啦
故此需要在导出 pdf 前,给元素添加一个图片的样式限定
element =`styleimg{max-width:100%;max-height:100%;vertical-align: middle;height: auto!important;width: auto!important;margin:10px0;}/style` + element;完整代码:
templatedivclass="container" button@click="generatePDF"下载PDF/button/div/templatescriptsetupimporthtml2pdffrom'html2pdf.js'asyncfunctionconvertImagesToBase64(htmlString) {// 创建一个临时DOM元素来解析HTMLconsttempDiv =document.createElement('div'); tempDiv.innerHTML= htmlString;
// 获取所有图片元素constimages = tempDiv.querySelectorAll('img');
// 遍历每个图片并转换for(constimgofimages) { try{ constbase64 =awaitgetBase64FromUrl(img.src); img.src= base64; }catch(error) { console.error(`无法转换图片${img.src}:`, error); // 保留原始URL如果转换失败 } }
// 返回转换后的HTMLreturntempDiv.innerHTML;}// 图片转base64functiongetBase64FromUrl(url) {returnnewPromise((resolve, reject) ={ constimg =newImage(); img.crossOrigin='Anonymous';// 处理跨域问题
img.onload=() ={ constcanvas =document.createElement('canvas'); canvas.width= img.width; canvas.height= img.height;
constctx = canvas.getContext('2d'); ctx.drawImage(img,0,0);
// 获取Base64数据 constdataURL = canvas.toDataURL('image/png'); resolve(dataURL);
img.onerror=() ={ reject(newError('图片加载失败'));
img.src= url;}
// 使用示例letelement =` div img src='http://t13.baidu.com/it/u=2041049195,1001882902&fm=224&app=112&f=JPEG?w=500&h=500' style="width: 300px;" / 职业:前端 技能:唱、跳、rap /div`;
functiongeneratePDF() { element =`style img { max-width: 100%; max-height: 100%; vertical-align: middle; height: auto !important; width: auto !important; margin: 10px 0; } /style` + element;convertImagesToBase64(element) .then(convertedHtml={ // 配置选项 constopt = { margin: 10, filename: '前端大法好.pdf', image: {type:'jpeg',quality:0.98}, html2canvas: {scale:2}, jsPDF: {unit:'mm',format:'a4',orientation:'portrait'}
// 生成PDF html2pdf().from(convertedHtml).set(opt).save(); }) .catch(error={ console.error('转换过程中出错:', error);}/script3.31 更新前端和后端联合解决方案评论区有位热心市民一名爱小惠的前端[2]提到:就让后端做,你在前端渲染出来后,让它提供一个接口,你在把 innerHTML 发给它(浏览器自动补全了)
基于这个思路,我尝试了前端补全 html,提交到后端,后端则可顺利 pdf 流
// 使用示例,纯在标签闭合不完整的情况let element = `div imgsrc='http://t13.baidu.com/it/u=2041049195,1001882902&fm=224&app=112&f=JPEG?w=500&h=500'style="width: 300px;" p职业:前端/p p技能:唱、跳、rap/p /div`;
function generatePDF() { element =`styleimg{ max-width:100%; max-height:100%; vertical-align: middle; height: auto!important; width: auto!important; margin:10px0; }/style` + element; const tempDiv = document.createElement('div'); tempDiv.innerHTML = element; // 获取到完整的html结构,提交给后端处理 console.log(tempDiv.innerHTML);}后端解决方案使用com.itextpdf.html2pdf解决
dependency groupIdcom.itextpdf/groupId artifactIdhtml2pdf/artifactId version5.0.2/version/dependency闹腾一圈下来,原来后端也有自己的 html2pdf 库,不过这个库不需要将图片转成base64,但需要设置中文字体,不设置的话中文将解析异常
后话前天提的需求,昨天兜的底,今天写的文章记录
这种问题,理应该后端处理,但后端和我吐槽过他处理起来的困难与问题,寻求前端帮助时,我也会积极配合。可在现实中,我遇过很多后端,死活不愿意配合前端,例如日期格式化、数据 id 类型 bigint 过大不字符化返回给前端等等,主打一个本着前端可以做就前端做的原则,说实在:属实下头
前后端本应该就是相互打配合的关系,谁方便就行个方便,没必要僵持不下
今天的分享就到此结束,如果你对技术 / 行业交流有兴趣,欢迎留言讨论哦!
关注更多AI编程资讯请去AI Coding专区:https://juejin.cn/aicoding
阅读原文
网站开发网络凭借多年的网站建设经验,坚持以“帮助中小企业实现网络营销化”为宗旨,累计为4000多家客户提供品质建站服务,得到了客户的一致好评。如果您有网站建设、网站改版、域名注册、主机空间、手机网站建设、网站备案等方面的需求...
请立即点击咨询我们或拨打咨询热线:13245491521 13245491521 ,我们会详细为你一一解答你心中的疑难。 项目经理在线