注册
web

这次终于轮到前端给后端兜底了🤣

封面.png


需求交代


最近我们项目组开发了个互联网采集的功能,也就是后端合理抓取了第三方的文章,数据结构大致如下:


<h1>前端人</h1>
<p>学好前端,走遍天下都不怕</p>

数据抓取到后,存储到数据库,然后前端请求接口获取到数据,直接在页面预览


<div v-html='articleContent'></div>

image.png


整个需求已经交代清楚


这个需求有点为难后端了


前天,客户说要新增一个文章的pdf导出功能,但就是这么一个合情合理的需求,却把后端为难住了,原因是部分数据采集过来的结构可能是这样的:


<h1>前端人</h1>
<p>学好前端,走遍天下都不怕</p>
<div>前端强,前端狂,交互特效我称王!</div
<p>JS 写得好,需求改不了!</p>
<p>React Vue 两手抓,高薪 offer 到你家!</p>
<p>浏览器里横着走, bug 见我都绕道!</p>
<p>Chrome 调试一声笑, IE 泪洒旧时光!</p>
<span>Git 提交不留情,版本回退我最行!

仔细的人就能发现问题了,很多html元素存在没有完整的闭合情况


image.png


但浏览器是强大的,丝毫不影响渲染效果,原来浏览器自动帮我们补全结构了


image.png


可后端处理这件事就没那么简单了,爬取到的数据也比我举例的要复杂的多,使用第三方插件将html转pdf时会识别标签异常等问题,因此程序会抛异常


来自后端的建议


苦逼的后端折腾了很久,还是没折腾出来,终于他发现前端页面有个右键打印的功能,也就是:


image.png


于是他说:浏览器这玩意整挺好啊,前端能不能研究研究,尝试从前端实现导出


那就研究研究


我印象中,确实有个叫vue-print-nb的前端插件,可以实现这个功能


但.......等等,这个插件仅仅是唤起打印的功能,我总不能真做成这样,让用户另存为pdf吧


于是,只能另辟蹊径,终于我找到了这么个仓库:github.com/burc-li/vue…


里面实现了dom元素导出pdf的功能


image.png


image.png


效果很不错,技术用到了jspdfhtml2canvas这两个第三方库,代码十分简单


const downLoadPdfA4Single = () => {
const pdfContaniner = document.querySelector('#pdfContaniner')
html2canvas(pdfContaniner).then(canvas => {
// 返回图片dataURL,参数:图片格式和清晰度(0-1)
const pageData = canvas.toDataURL('image/jpeg', 1.0)

// 方向纵向,尺寸ponits,纸张格式 a4 即 [595.28, 841.89]
const A4Width = 595.28
const A4Height = 841.89 // A4纸宽
const pageHeight = A4Height >= A4Width * canvas.height / canvas.width ? A4Height : A4Width * canvas.height / canvas.width
const pdf = new jsPDF('portrait', 'pt', [A4Width, pageHeight])

// addImage后两个参数控制添加图片的尺寸,此处将页面高度按照a4纸宽高比列进行压缩
pdf.addImage(
pageData,
'JPEG',
0,
0,
A4Width,
A4Width * canvas.height / canvas.width,
)
pdf.save('下载一页PDF(A4纸).pdf')
})
}

技术流程大致就是:



  • dom -> canvas
  • canvas -> image
  • image -> pdf

似乎一切都将水到渠成了


困在眼前的难题


这个技术栈,最核心的就是:必须要用到dom元素渲染


如果你尝试将打印的元素设置样式:


display: none;


visibility: hidden;



opacity: 0;

执行导出功能都将抛异常或者只能导出一个空白的pdf


这时候有人会问了:为什么要设置dom元素为不可见?


试想一下,你做了一个导出功能,总不能让客户必须先打开页面等html渲染完后,再导出吧?


客户的理想状态是:在列表的操作列里,有个导出按钮,点击就可以导出pdf了


何况还需要实现批量勾选导出的功能,总不能程序控制,导出一个pdf就open一个窗口渲染html吧


寻找新方法


此路不通,就只能重新寻找新的方向,不过也没费太多功夫,就找到了另外一个插件html2pdf.js解决了这事


这插件用起来也极其简单


npm install html2pdf.js

<template>
<div class="container">
<button @click="generatePDF">下载PDF</button>
</div>
</template>
<script setup>
import html2pdf from 'html2pdf.js'

// 使用示例
let element = `
<h1>前端人</h1>
<p>学好前端,走遍天下都不怕</p>
<div>前端强,前端狂,交互特效我称王!</div
<p>JS 写得好,需求改不了!</p>
<p>React Vue 两手抓,高薪 offer 到你家!</p>
<p>浏览器里横着走, bug 见我都绕道!</p>
<p>Chrome 调试一声笑, IE 泪洒旧时光!</p>
<span>Git 提交不留情,版本回退我最行!
`
;

function generatePDF() {
// 配置选项
const opt = {
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>


功能正常,似乎一切都完美


image.png


问题没有想的那么简单


如果我们的html是纯文本元素,这程序跑起来没有任何问题,但我们抓取的信息都源于互联网,html结构怎么可能会这么简单?如果我们的html中包含图片信息,例如:


// 使用示例
let element = `
<div>
<img src='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,图片占位处是个空白块


image.png



思考一下:类似案例中的图片加载方式,都是get方式的异步请求,而异步请求就会导致图片还没渲染完成,但导出的程序已经执行完成情况(最直接的观察方式就是,把这个元素放到浏览器上渲染,会发现图片也是过一会才慢慢加载完成的)


不过我不确定html2pdf.js这个插件是否会发起图片请求,但不管发不发起,导出的行为明显是在图片渲染前完成的,就导致了这个空白块的存在



问题分析完了,那就解决吧


既然图片异步加载不行,那就使用图片同步加载吧


不是吧,你问我:什么是图片同步加载?我也不晓得,这个词是我自己当下凭感觉造的,如有雷同,纯属巧合了


那我理解的图片同步加载是什么意思呢?简单来说,就是将图片转成Base64,因为这种方式,即使说无网的情况也能正常加载图片,因此我凭感觉断定,这就是图片同步加载


基于这个思路,我写了个demo


<template>
<div class="container">
<button @click="generatePDF">下载PDF</button>
</div>
</template>
<script setup>
import html2pdf from 'html2pdf.js'

async function convertImagesToBase64(htmlString) {
// 创建一个临时DOM元素来解析HTML
const tempDiv = document.createElement('div');
tempDiv.innerHTML = htmlString;

// 获取所有图片元素
const images = tempDiv.querySelectorAll('img');

// 遍历每个图片并转换
for (const img of images) {
try {
const base64 = await getBase64FromUrl(img.src);
img.src = base64;
} catch (error) {
console.error(`无法转换图片 ${img.src}:`, error);
// 保留原始URL如果转换失败
}
}

// 返回转换后的HTML
return tempDiv.innerHTML;
}

// 图片转base64
function getBase64FromUrl(url) {
return new Promise((resolve, reject) => {
const img = new Image();
img.crossOrigin = 'Anonymous'; // 处理跨域问题

img.onload = () => {
const canvas = document.createElement('canvas');
canvas.width = img.width;
canvas.height = img.height;

const ctx = canvas.getContext('2d');
ctx.drawImage(img, 0, 0);

// 获取Base64数据
const dataURL = canvas.toDataURL('image/png');
resolve(dataURL);
};

img.onerror = () => {
reject(new Error('图片加载失败'));
};

img.src = url;
});
}

// 使用示例
let element = `
<div>
<img src='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() {
convertImagesToBase64(element)
.then(convertedHtml => {
// 配置选项
const opt = {
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链接必须是同源或者允许跨越的,否则就会存在图片加载异常的问题


修复图片过大的问题


部分图片的宽度会过大,导致图片加载不全的问题,这在预览的情况下也存在


image.png


因为需要加上样式限定


img {
max-width: 100%;
max-height: 100%;
vertical-align: middle;
height: auto !important;
width: auto !important;
margin: 10px 0;
}

这样就正常啦


image.png


故此需要在导出pdf前,给元素添加一个图片的样式限定


element =`<style>
img {
max-width: 100%;
max-height: 100%;
vertical-align: middle;
height: auto !important;
width: auto !important;
margin: 10px 0;
}
</style>`
+ element;

完整代码:


<template>
<div class="container">
<button @click="generatePDF">下载PDF</button>
</div>
</template>
<script setup>
import html2pdf from 'html2pdf.js'
async function convertImagesToBase64(htmlString) {
// 创建一个临时DOM元素来解析HTML
const tempDiv = document.createElement('div');
tempDiv.innerHTML = htmlString;

// 获取所有图片元素
const images = tempDiv.querySelectorAll('img');

// 遍历每个图片并转换
for (const img of images) {
try {
const base64 = await getBase64FromUrl(img.src);
img.src = base64;
} catch (error) {
console.error(`无法转换图片 ${img.src}:`, error);
// 保留原始URL如果转换失败
}
}

// 返回转换后的HTML
return tempDiv.innerHTML;
}
// 图片转base64
function getBase64FromUrl(url) {
return new Promise((resolve, reject) => {
const img = new Image();
img.crossOrigin = 'Anonymous'; // 处理跨域问题

img.onload = () => {
const canvas = document.createElement('canvas');
canvas.width = img.width;
canvas.height = img.height;

const ctx = canvas.getContext('2d');
ctx.drawImage(img, 0, 0);

// 获取Base64数据
const dataURL = canvas.toDataURL('image/png');
resolve(dataURL);
};

img.onerror = () => {
reject(new Error('图片加载失败'));
};

img.src = url;
});
}

// 使用示例
let element = `
<div>
<img src='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 =`<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 => {
// 配置选项
const opt = {
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>


后话


前天提的需求,昨天兜的底,今天写的文章记录


这种问题,理应该后端处理,但后端和我吐槽过他处理起来的困难与问题,寻求前端帮助时,我也会积极配合。可在现实中,我遇过很多后端,死活不愿意配合前端,例如日期格式化、数据id类型bigint过大不字符化返回给前端等等,主打一个本着前端可以做就前端做的原则,说实在:属实下头


前后端本应该就是相互打配合的关系,谁方便就行个方便,没必要僵持不下


今天的分享就到此结束,如果你对技术/行业交流有兴趣,欢迎添加howcoder微信,邀你进群交流


往期精彩


《你不了解的Grid布局》


《就你小子还不会 Grid布局是吧?》


《超硬核:从零到一部署指南》


《私活2年,我赚到了人生的第一桶金》


《接入AI后,开源项目瞬间有趣了😎》


《肝了两个月,我们无偿开源了》


《彻底不NG前端路由》


《vue项目部署自动检测更新》


《一个公告滚动播放功能引发的背后思考》


《前端值得学习的开源socket应用》


作者:howcode
来源:juejin.cn/post/7486440418139652137

0 个评论

要回复文章请先登录注册