使用 html2canvas & jspdf 后的总结

使用 html2canvas & jspdf 后的总结

| 前言

​ 接到一个需求,需要在前端页面增加下载按钮,点击后能够将页面中的内容导出成PDF文件。但项目是一个React项目,并且该系统是后端同事在网上找的开源系统,整个项目的结构很难去梳理,并且基本没有注释,所以遇到很多问题。

| 遇到的问题
  • 跨域问题(本项目未遇到,但属于很常见的问题之一):由于浏览器的安全策略,HTML2Canvas 在跨域场景下可能会受到限制。如果要捕获或渲染来自不同域的内容,可能需要通过 CORS(跨域资源共享)或代理服务器来解决跨域问题。
  • 对复杂页面的支持:HTML2Canvas 非常适用于简单的静态页面,但对于复杂的动态页面,如包含大量 JavaScript 动画或通过 AJAX 更新的内容,它可能无法完全捕获和呈现所有元素。
  • 存在图片、文字、表格被分割的现象,需要分页处理;
  • jspdf 在对图片进行处理时,图片过大会导致导出的PDF文件出现黑屏问题。
  • html2canvas无法处理iframe标签中的内容,会导致内容丢失的问题;
| 代码展示
  1. 下载依赖插件
npm install html2canvas --save

npm install jsPDF --save

也可以使用 html2pdf 库,这个其实就是封装的以上两个库。

npm install html2pdf --save

  1. 封装导出方法

创建一个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. 下载功能实现

    在实现下载功能上,因为这个系统的特殊性质,有必要简述一下项目的特点:

    (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)
      }
    };

| 总结
  1. 这两个库结合在一起用其实应该能够满足大部分的需求,当然其中的坑也很多,特别是jspdf,其中产生的问题也千奇百怪,在这个项目经历中,我只是遇到了黑屏问题,解决完后,也觉得自己蛮傻的。我当时遇到这个问题时,考虑的思路是这样的:

    是否是遮罩造成了黑屏 (不是)=> 是否是内容过长导致 html2canvas 的问题 (不是)=> 发现是jspdf结合大体积图片导致的黑屏

  2. 关于html2canvas&jspdf的分页以及图表和图片切割问题,代码展示2其实提供的是A4大小的分页实例,能够实现分页,但也同样存在无法解决将图表和图片切割的问题,由于我的项目需求特殊,图表和图片切割问题是通过手动调整格式解决(非常繁琐)。不过在查阅相关解决办法时,有看到通过固定好内容格式来避免切割问题的,这也算一个思路;还有大佬通过代码来实现的,各个地方都能够查阅到,我也并没有仔细研究,故只是提供一个途径。

  3. 关于iframe标签导致的截图失效问题,可以考虑将iframe的内容通过dom-to-image库来转成图片后解决。还有一个就是图片跨域问题导致的失效,html2canvas提供了一个useCORS的属性,将值设置成true后启用,具体的可以参考后面的链接地址。

| 参考文章
  1. 《深度使用html2canvas的经验总结》

    https://juejin.cn/post/7209862607975563301?searchId=202310101115274A5A86F3341EC09A31B6

  2. 《html2canvas总是跨域,一步到位》

    https://juejin.cn/post/7062223088695377934?searchId=202310101115274A5A86F3341EC09A31B6

  3. 《vue + html2canvas + jsPDF实现A4分页截断的解决方案》

    https://juejin.cn/post/7276023998566973451?searchId=202310101115274A5A86F3341EC09A31B6

  4. 《html2canvas使用文档》

    https://juejin.cn/post/7207342547361267771

  5. 《React结合html2canvas - jspdf 截取表格转pdf下载 并上传后台》

    https://blog.csdn.net/weixin_42508580/article/details/111475051

  6. 《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~

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值