使用 html2canvas & jspdf 后的总结
| 前言
接到一个需求,需要在前端页面增加下载按钮,点击后能够将页面中的内容导出成PDF文件。但项目是一个React项目,并且该系统是后端同事在网上找的开源系统,整个项目的结构很难去梳理,并且基本没有注释,所以遇到很多问题。
| 遇到的问题
- 跨域问题(本项目未遇到,但属于很常见的问题之一):由于浏览器的安全策略,HTML2Canvas 在跨域场景下可能会受到限制。如果要捕获或渲染来自不同域的内容,可能需要通过 CORS(跨域资源共享)或代理服务器来解决跨域问题。
- 对复杂页面的支持:HTML2Canvas 非常适用于简单的静态页面,但对于复杂的动态页面,如包含大量 JavaScript 动画或通过 AJAX 更新的内容,它可能无法完全捕获和呈现所有元素。
- 存在图片、文字、表格被分割的现象,需要分页处理;
- jspdf 在对图片进行处理时,图片过大会导致导出的PDF文件出现黑屏问题。
- html2canvas无法处理iframe标签中的内容,会导致内容丢失的问题;
| 代码展示
- 下载依赖插件
npm install html2canvas --save
npm install jsPDF --save
也可以使用 html2pdf 库,这个其实就是封装的以上两个库。
npm install html2pdf --save
- 封装导出方法
创建一个ts文件,用于封装导出方法,具体代码如下:
import html2canvas from 'html2canvas';
import JsPDF from 'jspdf';
const ExportSavePdf = function(elementName:string, htmlTitle:string, currentTime:string) {
var element = document.getElementById(elementName)
if(element){
html2canvas(element, {
logging: false,
scale: window.devicePixelRatio * 3, // 增加清晰度
}).then(function(canvas) {
// 参数:A4纸,纵向"p"; 横向"l"
var pdf = new JsPDF("p", "mm", "a4")
var ctx = canvas.getContext("2d")
// A4大小,210mm x 297mm,四边各保留20mm的边距
var a4w = 200;
var a4h = 287
// 按A4显示比例换算一页图像的像素高度
var imgHeight = Math.floor(a4h * canvas.width / a4w)
var renderedHeight = 0
// 进行分页处理
while (renderedHeight < canvas.height) {
var page = document.createElement("canvas");
page.width = canvas.width;
// 可能内容不足一页
page.height = Math.min(imgHeight, canvas.height - renderedHeight)
if(page){
// 使用类型断言将 getImageData 结果断言为 ImageData 类型
const imageData = ctx?.getImageData(
0,
renderedHeight,
canvas.width,
Math.min(imgHeight, canvas.height - renderedHeight)
) as ImageData;
// 用getImageData剪裁指定区域,并画到前面创建的canvas对象中
page?.getContext("2d")?.putImageData(imageData, 0, 0);
// 添加图像到页面,保留10mm边距
pdf.addImage(page.toDataURL("image/jpeg", 1.0), "JPEG", 5, 5, a4w, Math.min(a4h, a4w * page.height / page.width))
renderedHeight += imgHeight;
// 如果后面还有内容,添加一个空页
if (renderedHeight < canvas.height) { pdf.addPage() }
// delete page;
}
}
pdf.save(htmlTitle + currentTime)
})
}
}
export default ExportSavePdf;
注意!
在使用 html2canvas
进行截图时,我的配置项里有一个scale: window.devicePixelRatio * 3
,其目的是为了增强导出的PDF文件的清晰度,也正是这个配置,导致生成的canvas元素质量过大(大概是这样的原因,具体的深层的原因暂时没有找到),如果被导出的dom内容不多,导出的PDF文件正常,但体积在24mb左右;一旦dom内容多了,导出的PDF文件就会出现黑屏问题。经过(不规范的)测试,html2canvas
能够正常截图,这里主要是因为jspdf
库的问题,怀疑是jspdf
无法处理体积过大的图片(或是canvas元素)。
当去掉配置项中的scale: window.devicePixelRatio * 3
后, 便能够正常的进行打印。
-
下载功能实现
在实现下载功能上,因为这个系统的特殊性质,有必要简述一下项目的特点:
(1) 需求是打印页面指定元素(以下称为DOM)中的内容,内容是动态生成的一个一个“块”,类似于低代码开发的那种模式,块中的内容也是通过遍历加组件去生成的,里面的嵌套很深;
(2)块内的图表则是嵌套在iframe中,这也导致了
html2canvas
无法获取其中的内容;
功能代码实现如下:
// 页面打印按钮功能
const downloadToPdf = () => {
// target是外部容器,reportContent是内容本体
const target : any = document.querySelectorAll(".targetBox")[0];
const reportContent : any = document.querySelectorAll(".reportContent")[0];
// overlay是下载时的遮罩,downloadButton是下载按钮
const overlay : any = document.querySelector(".overlay");
const downloadButton : any = document.querySelector("#downloadToPDF");
// 因为后面使用 jspdf 时要给目标节点的id,所以动态设置id
reportContent?.setAttribute("id","reportToDownload");
overlay.style.display = "block";
downloadButton.disabled = true;
downloadButton.textContent = "下载中...";
downloadButton.classList.replace("download-btn","downloading-btn")
if( target && reportContent ){
// 因为页面内容是懒加载,所以要先自动地缓慢地滚动到底,让内容加载出来
let distance = target.scrollTop;
let maxDistance = reportContent.offsetHeight;
let timer = setInterval(()=>{
target?.scrollTo({
top:distance,
behavior:"smooth",
})
if(distance < maxDistance){
distance += 200;
}else if(distance >= maxDistance){
clearInterval(timer)
// 到底部后(全部内容加载完)获取所有的图表iframe标签
const nodes = document.querySelectorAll('iframe');
// 将所有图表转成图片,再打印下载pdf(PS:由于dom-to-image库是异步操作,所以用Promise)
let arr: Promise<any>[] = []
nodes.forEach((item:any)=>{
const promise =new Promise((resolve)=>{
domtoimage.toPng(item.contentWindow?.document.querySelector(".frame-content"), {})
.then(function (dataUrl) {
var img = new Image();
img.src = dataUrl;
// 在原来<iframe>同级位置插入图表图片,并隐藏<iframe>
item.parentNode.appendChild(img);
item.style.display = "none";
resolve(dataUrl); // 只是凝结状态
});
})
arr.push(promise)
})
// 通过Promise.all集中处理转图片的操作后,就可以导出成pdf
Promise.all(arr).then(()=>{
ExportSavePdf("reportToDownload","document","");
reportContent?.removeAttribute("id");
// 打印完成后,将图表复原,并滚回顶部
nodes.forEach((item:any)=>{
const img = item.nextSibling
item.parentNode.removeChild(img);
item.style.display = "block";
});
target?.scrollTo({
top:0,
})
overlay.style.display = "none";
downloadButton.disabled = false;
downloadButton.textContent = "下载报告";
downloadButton.classList.replace("downloading-btn","download-btn")
});
}
},300)
}
};
| 总结
-
这两个库结合在一起用其实应该能够满足大部分的需求,当然其中的坑也很多,特别是
jspdf
,其中产生的问题也千奇百怪,在这个项目经历中,我只是遇到了黑屏问题,解决完后,也觉得自己蛮傻的。我当时遇到这个问题时,考虑的思路是这样的: 是否是遮罩造成了黑屏 (不是)=> 是否是内容过长导致 html2canvas 的问题 (不是)=> 发现是jspdf结合大体积图片导致的黑屏
-
关于
html2canvas
&jspdf
的分页以及图表和图片切割问题,代码展示2其实提供的是A4大小的分页实例,能够实现分页,但也同样存在无法解决将图表和图片切割的问题,由于我的项目需求特殊,图表和图片切割问题是通过手动调整格式解决(非常繁琐)。不过在查阅相关解决办法时,有看到通过固定好内容格式来避免切割问题的,这也算一个思路;还有大佬通过代码来实现的,各个地方都能够查阅到,我也并没有仔细研究,故只是提供一个途径。 -
关于iframe标签导致的截图失效问题,可以考虑将iframe的内容通过
dom-to-image
库来转成图片后解决。还有一个就是图片跨域问题导致的失效,html2canvas
提供了一个useCORS
的属性,将值设置成true
后启用,具体的可以参考后面的链接地址。
| 参考文章
-
《深度使用html2canvas的经验总结》
https://juejin.cn/post/7209862607975563301?searchId=202310101115274A5A86F3341EC09A31B6
-
《html2canvas总是跨域,一步到位》
https://juejin.cn/post/7062223088695377934?searchId=202310101115274A5A86F3341EC09A31B6
-
《vue + html2canvas + jsPDF实现A4分页截断的解决方案》
https://juejin.cn/post/7276023998566973451?searchId=202310101115274A5A86F3341EC09A31B6
-
《html2canvas使用文档》
https://juejin.cn/post/7207342547361267771
-
《React结合html2canvas - jspdf 截取表格转pdf下载 并上传后台》
https://blog.csdn.net/weixin_42508580/article/details/111475051
-
《HTML2CANVAS中文文档》
https://allenchinese.github.io/html2canvas-docs-zh-cn/docs/html2canvas-getStart.html
I’m rookie front end developer…, just want to share some small ideas with someone need help.
hope world peace~