纯前端实现dom节点转pdf文件

112 阅读8分钟

接到的需求

需要能够将页面上已有的dom转为pdf并下载,能够满足单个下载和批量下载

实现思路

  • 将dom转为图片
  • 图片转为pdf
  • 单个下载直接下载
  • 多个下载添加到压缩包下载

代码实现

将dom转换为图片

我这里使用的是html2canvas

<script src="https://cdnjs.cloudflare.com/ajax/libs/html2canvas/1.4.1/html2canvas.min.js"></script>

<script>
// 这里就可以拿到生成的canvas图片
const canvas = await html2canvas(document.querySelector('.fm-img'))
const pageData = canvas.toDataURL('image/png');
</script>

图片转为pdf

// pdf用的jspdf
<script src="https://cdnjs.cloudflare.com/ajax/libs/jspdf/2.3.1/jspdf.umd.min.js"></script>
<script>
    let A4_WIDTH = 595;
    let A4_HEIGHT = 842;
    const pdf = new jspdf.jsPDF("", "pt", "a4");
    let contentWidth = canvas.width; // 生成的canvas图片整体宽度
    let contentHeight = canvas.height; // 生成的canvas图片整体高度
    let position = 0; // 开始的位置
    let imgWidth = A4_WIDTH - 12 // -12为了页面有右边距
    let imgHeight = (A4_WIDTH / contentWidth) * contentHeight
    // 单页情况处理 
    // pageData是上面拿到的png内容、 JPEG 图片格式、
    // 6 为添加到的pdfx轴的位置、40是y轴的位置,距离上边距、imgWidth图片宽度、imgHeight渲染的图片高度
    pdf.addImage(pageData, 'JPEG', 6, 40, imgWidth, imgHeight);
    
    pdf.save('文件名.pdf')
    
</script>

多页批量导出

<!DOCTYPE HTML>
<html>
<head>
<meta content="text/html; charset=utf-8" http-equiv="Content-Type"/>
<script src="https://code.jquery.com/jquery-3.6.4.js"></script>  
<script src="https://cdnjs.cloudflare.com/ajax/libs/jspdf/2.3.1/jspdf.umd.min.js"></script>
<!-- <script src="https://cdnjs.cloudflare.com/ajax/libs/jspdf/2.3.5/jspdf.umd.min.js"></script> -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/html2canvas/1.4.1/html2canvas.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jszip/3.4.0/jszip.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/FileSaver.js/2.0.0/FileSaver.min.js"></script>



</head>
<body>
<!-- 封面 -->
  <div><img style="height:842;width: 595px" src="./fm.png" class='fm-img' /></div>
  <div><img style="height:842;width: 595px" src="./fg.png" class='fg-img' /></div>
  <div class="content">
    <h1>春日里的温柔时光</h1>  
    <p>  
        春风轻拂,万物复苏,大地披上了一袭嫩绿的新装,仿佛一夜之间,世界被温柔地唤醒。在这个充满生机的季节里,我独自漫步于乡间小径,享受着春日里独有的温柔时光。  
    </p>  
    <p>  
        阳光透过稀疏的云层,洒下斑驳陆离的光影,给这静谧的田野增添了几分暖意。远处,一片片金黄色的油菜花海随风起伏,宛如金色的波浪,散发着淡淡的清香,吸引着蜜蜂和蝴蝶在花间穿梭,它们忙碌的身影,似乎在为这春日的画卷添上灵动的一笔。  
    </p>  
    <p>  
        脚下的泥土松软而湿润,每一步都踏出了春天的声音。小草从土里探出头来,好奇地打量着这个世界,它们虽不起眼,却以顽强的生命力,铺就了一条绿色的地毯,让人不忍践踏。偶尔,几朵野花点缀其间,红的、黄的、蓝的……它们虽不名贵,却以独特的姿态,展现着春天的多彩与活力。  
    </p>  
    <p>  
        沿着小路前行,一条清澈见底的小溪映入眼帘。溪水潺潺,发出悦耳的声响,像是大自然最悠扬的乐章。溪边,柳树依依,嫩绿的枝条轻拂水面,仿佛少女在梳洗着秀发。我不由自主地停下脚步,坐在溪边的石头上,闭上眼睛,让心灵随着这清澈的流水一同流淌,感受那份宁静与和谐。  
    </p>  
    <p>  
        此时此刻,我仿佛与世隔绝,所有的烦恼与忧愁都随风而去,只留下心灵的纯净与宁静。我深深地吸了一口气,空气中弥漫着泥土与花草的混合气息,那是春天的味道,是生命的味道,让人心旷神怡,陶醉不已。  
    </p>  
    <p>  
        春日里的温柔时光,是如此的短暂而珍贵。它让我们感受到了大自然的恩赐,也让我们体会到了生活的美好与希望。让我们珍惜这份温柔,用心去感受每一个春天的到来,用爱去呵护这个美好的世界。  
    </p>  
  </div>
  <div class="content">
    <h1>春日里的温柔时光</h1>  
    <p>  
        春风轻拂,万物复苏,大地披上了一袭嫩绿的新装,仿佛一夜之间,世界被温柔地唤醒。在这个充满生机的季节里,我独自漫步于乡间小径,享受着春日里独有的温柔时光。  
    </p>  
    <p>  
        阳光透过稀疏的云层,洒下斑驳陆离的光影,给这静谧的田野增添了几分暖意。远处,一片片金黄色的油菜花海随风起伏,宛如金色的波浪,散发着淡淡的清香,吸引着蜜蜂和蝴蝶在花间穿梭,它们忙碌的身影,似乎在为这春日的画卷添上灵动的一笔。  
    </p>  
    <p>  
        脚下的泥土松软而湿润,每一步都踏出了春天的声音。小草从土里探出头来,好奇地打量着这个世界,它们虽不起眼,却以顽强的生命力,铺就了一条绿色的地毯,让人不忍践踏。偶尔,几朵野花点缀其间,红的、黄的、蓝的……它们虽不名贵,却以独特的姿态,展现着春天的多彩与活力。  
    </p>  
    <p>  
        沿着小路前行,一条清澈见底的小溪映入眼帘。溪水潺潺,发出悦耳的声响,像是大自然最悠扬的乐章。溪边,柳树依依,嫩绿的枝条轻拂水面,仿佛少女在梳洗着秀发。我不由自主地停下脚步,坐在溪边的石头上,闭上眼睛,让心灵随着这清澈的流水一同流淌,感受那份宁静与和谐。  
    </p>  
    <p>  
        此时此刻,我仿佛与世隔绝,所有的烦恼与忧愁都随风而去,只留下心灵的纯净与宁静。我深深地吸了一口气,空气中弥漫着泥土与花草的混合气息,那是春天的味道,是生命的味道,让人心旷神怡,陶醉不已。  
    </p>  
    <p>  
        春日里的温柔时光,是如此的短暂而珍贵。它让我们感受到了大自然的恩赐,也让我们体会到了生活的美好与希望。让我们珍惜这份温柔,用心去感受每一个春天的到来,用爱去呵护这个美好的世界。  
    </p>  
  </div>
  <div class="content">
    <h1>春日里的温柔时光</h1>  
    <p>  
        春风轻拂,万物复苏,大地披上了一袭嫩绿的新装,仿佛一夜之间,世界被温柔地唤醒。在这个充满生机的季节里,我独自漫步于乡间小径,享受着春日里独有的温柔时光。  
    </p>  
    <p>  
        阳光透过稀疏的云层,洒下斑驳陆离的光影,给这静谧的田野增添了几分暖意。远处,一片片金黄色的油菜花海随风起伏,宛如金色的波浪,散发着淡淡的清香,吸引着蜜蜂和蝴蝶在花间穿梭,它们忙碌的身影,似乎在为这春日的画卷添上灵动的一笔。  
    </p>  
    <p>  
        脚下的泥土松软而湿润,每一步都踏出了春天的声音。小草从土里探出头来,好奇地打量着这个世界,它们虽不起眼,却以顽强的生命力,铺就了一条绿色的地毯,让人不忍践踏。偶尔,几朵野花点缀其间,红的、黄的、蓝的……它们虽不名贵,却以独特的姿态,展现着春天的多彩与活力。  
    </p>  
    <p>  
        沿着小路前行,一条清澈见底的小溪映入眼帘。溪水潺潺,发出悦耳的声响,像是大自然最悠扬的乐章。溪边,柳树依依,嫩绿的枝条轻拂水面,仿佛少女在梳洗着秀发。我不由自主地停下脚步,坐在溪边的石头上,闭上眼睛,让心灵随着这清澈的流水一同流淌,感受那份宁静与和谐。  
    </p>  
    <p>  
        此时此刻,我仿佛与世隔绝,所有的烦恼与忧愁都随风而去,只留下心灵的纯净与宁静。我深深地吸了一口气,空气中弥漫着泥土与花草的混合气息,那是春天的味道,是生命的味道,让人心旷神怡,陶醉不已。  
    </p>  
    <p>  
        春日里的温柔时光,是如此的短暂而珍贵。它让我们感受到了大自然的恩赐,也让我们体会到了生活的美好与希望。让我们珍惜这份温柔,用心去感受每一个春天的到来,用爱去呵护这个美好的世界。  
    </p>  
  </div>
<script>  
  $(document).ready(function() { 
      // 当前没下载的内容
      let downloadArr = [];

      function html2pdf() {  
          let zip = new JSZip();
          let taskArr = [];
          var elements = document.querySelectorAll('.content')
          for (let i = 0; i < elements.length; i++) {
            downloadArr.push({
              isDownload: false,
              ele: elements[i],
              index: i
            })
          }

          handleDown(zip)
      }  

      function handleDown(zip) {
        let list = [];
        // 这里是限制每次处理的个数
        // 实践下来发现每次限制一个可能比较好
        const limit = 2;
        downloadArr.forEach(item => {
          if(!item.isDownload && (list.length < limit)) list.push(item)
        })

        const listIsOk = {isOk: false}
        const cancelFn = () => {listIsOk.isOk=true}

        const arr = list.map(item => {
          const type = $(item.ele).data('type');
          // 将所有节点创建异步任务
          return getDocImg(item, type, listIsOk)
        })

        Promise.race(arr).then((res) => {
          cancelFn()
          const {elementItem, pdf} = res;
          if(pdf) {
              let datauri = pdf.output('dataurlstring');
              let base64 = datauri.substring(28);
              // 处理每个下载名,需要你自己改
              zip.file(`Hello${elementItem.index}.pdf`, base64, {base64: true});
              elementItem.isDownload=true
          }
          const isSuccess = checkIsSuccess();
          if(!isSuccess) {
            handleDown(zip)
          } else {
            handleZipSave(zip)
          }
        })

       
      }

      function handleZipSave(zip) {
        zip.generateAsync({type:"blob"}).then(content => {
        // 下载名称
        // loading 结束
          saveAs(content, "download.zip");
        }).catch(err => {
        console.log(err)
        })
      }

      function checkIsSuccess() {
        return downloadArr.every(item => !!item.isDownload)
      }
  
      let A4_WIDTH = 595;
      let A4_HEIGHT = 842;
      
      // listIsOk 是打算处理多个,然后选择其中一个处理快的,优先返回,然后中断其他promise
      async function getDocImg(elementItem, type, listIsOk) {
          const {ele:element, index} = elementItem;
          const pdf = new jspdf.jsPDF("", "pt", "a4");
          // type为判断封面,有则添加封面
          if(type) {
           const res1 = await html2canvas(document.querySelector('.fm-img'))
           const pageData1 = res1.toDataURL('image/png');

           if(listIsOk.isOk) return

           pdf.addImage(pageData1, 'JPEG',0, 0, A4_WIDTH, A4_HEIGHT);
           pdf.addPage()
          }
          const canvas = await html2canvas(element)
          const pageData = canvas.toDataURL('image/png');
          let contentWidth = canvas.width
          let contentHeight = canvas.height
          debugger
          let pageHeightN = (contentWidth / A4_WIDTH) * A4_HEIGHT;
  
          let leftHeight = contentHeight
          let position = 0;
  
          let imgWidth = A4_WIDTH - 12 // -10为了页面有右边距
          let imgHeight = (A4_WIDTH / contentWidth) * contentHeight
  
          if (leftHeight <pageHeightN) { 
            if(listIsOk.isOk) return
              pdf.addImage(pageData, 'JPEG', 6, 40, imgWidth, imgHeight);
          } else { // 分页
              while (leftHeight > 0 && !listIsOk.isOk) {
                if(listIsOk.isOk) return
                  pdf.addImage(pageData, "JPEG", 6, position + 40, imgWidth, imgHeight)
                  leftHeight -= pageHeightN
                  position -= A4_HEIGHT
                  // 避免添加空白页
                  if (leftHeight > 0) {
                      pdf.addPage()
                  }
              }
              if(type) {
                if(listIsOk.isOk) return
                pdf.addPage()
                const res1 = await html2canvas(document.querySelector('.fg-img'))
                const pageData1 = res1.toDataURL('image/png');
                pdf.addImage(pageData1, 'JPEG',0,0, A4_WIDTH, A4_HEIGHT);
              }
          }
          return {pdf,elementItem}
      }
      // 调用函数  
      html2pdf();  
  });  
  </script>
</body>
</html>

目前存在的问题

  1. 批量下载太多,会很慢(有想到过用workjs解决,但是work进程无法传入dom节点作为参数,所以就放弃了)
  2. 数量过多会导致浏览器崩溃,因为zip这个变量多大,占用内容过多
  3. pdf截取图片问题,无法识别文字,会将文字截断.

上面的问题暂时没想到其他解决方案,建议一次性处理少量dom节点