美文网首页JavaScript前端开发那些事儿
vue页面生成pdf且避免分页截断处理

vue页面生成pdf且避免分页截断处理

作者: 无糖可乐i | 来源:发表于2020-08-17 10:57 被阅读0次

    要求按A4纸大小生成pdf,支持分页且内容不被截断

    • 使用html2canvas和jspdf插件实现
    • 通过html2canvas将HTML页面转换成图片,然后再通过jspdf将图片生成为pdf文件

    一、添加依赖模块

    npm install html2canvas --save
    npm install jspdf --save

    二、创建pdf.js全局函数文件

    // 创建 pdf.js
    // 引入依赖
    import Vue from 'vue'
    import html2canvas from 'html2canvas'
    import JsPDF from 'jspdf'
    
    const PDF = {}
    PDF.install = function (Vue, options) {
      /**
       * targetDom: 需要打印的dom对象
       * name:pdf的名字
       * callback: 回调函数
       */
      Vue.prototype.$createPdf = function (targetDom, name, callback) {
        let cloneDom = targetDom.cloneNode(true)
        cloneDom.style.width = targetDom.scrollWidth + 'px'
        cloneDom.style.height = targetDom.scrollHeight + 'px'
        cloneDom.style.background = '#FFFFFF'
        document.body.appendChild(cloneDom)
        let scale = 2
        html2canvas(copyDom, {
          scale: scale, // 提升画面质量,但是会增加文件大小
          useCORS: true, // 允许跨域图片 当图片是链接地址时,需加该属性,否组无法显示图片
          imageTimeout: 0, // 图片加载延迟,默认延迟为0,单位毫秒
          height: targetDom.scrollHeight, // canvas的高度设定
          width: targetDom.scrollWidth, // canvas的宽度设定
          dpi: window.devicePixelRatio * scale, // 将分辨率提高到特定的DPI
        }).then(canvas => {
          document.body.removeChild(cloneDom)
          // a4纸的尺寸[592.28,841.89]
          const A4_WIDTH = 592.28
          const A4_HEIGHT = 841.89
          let contentWidth = canvas.width
          let contentHeight = canvas.height
          let pageData = canvas.toDataURL('image/jpeg', 0.6)
          // 一页pdf html页面生成的canvas高度;
          let pageHeight = contentWidth / A4_WIDTH * A4_HEIGHT
          // 未生成pdf的html页面高度
          let leftHeight = contentHeight
          // 页面偏移
          let position = 0
          // a4纸的尺寸[592.28,841.89],html页面生成的canvas在pdf中图片的宽高
          let imgWidth = A4_WIDTH
          let imgHeight = A4_WIDTH / contentWidth * contentHeight
          // 设置pdf的尺寸,pdf要使用pt单位 已知 1pt/1px = 0.75   pt = (px/scale)* 0.75
          let pdf = new JsPDF('', 'pt', 'a4')
          // 有两个高度需要区分,一个是html页面的实际高度,和生成pdf的页面高度
          // 当内容未超过pdf一页显示的范围,无需分页
          if (leftHeight < pageHeight) {
            pdf.addImage(pageData, 'JPEG', 0, 0, imgWidth, imgHeight)
          } else {
            while (leftHeight > 0) {
              pdf.addImage(pageData, 'JPEG', 0, position, imgWidth, imgHeight)
              leftHeight -= pageHeight
              position -= A4_HEIGHT
              // 避免添加空白页
              if (leftHeight > 0) {
                pdf.addPage()
              }
            }
          }
          // 直接下载
          // pdf.save(name + '.pdf') 
          // base64转成文件
          let buffer = pdf.output('datauristring')
          let myfile = dataURLtoFile(buffer, name)
          callback(myfile)
        })
      }
    }
    Vue.use(PDF)
    export default PDF
    
    const dataURLtoFile = (dataUrl, fileName) => {
      let arr = dataUrl.split(',')
      let mime = arr[0].match(/:(.*?);/)[1]
      let bstr = atob(arr[1])
      let n = bstr.length
      let u8arr = new Uint8Array(n)
      while (n--) {
        u8arr[n] = bstr.charCodeAt(n)
      }
      return new File([u8arr], fileName, { type: mime })
    }
    

    三、在页面上使用

    // html
     <div ref="targetDom">
        ......
     </div>
    // js
    vm.$nextTick(async () => {
      let target = vm.$refs.targetDom
      vm.$createPdf(target, '文件名', async (file) => {
          // file对象
      })
    })
    

    四、处理分页被截断问题

    1、最简单的就是不截断pdf

    将DOM转换为canvas并将pdf的宽高设置为canvas的宽高,再将canvas转为图片,实例化jspdf,将内容图片完整的放在pdf中

    2、固定每页pdf大小为A4纸大小且内容不能给隔断

    在保持网页的宽高比以及pdf每页的大小(A4)的情况下,dom下元素越多,越可能出现生成多页pdf的情况,就很容易出现一个dom元素被截断分散在两页pdf里
    两种思路:
    1)每页pdf中的元素以及高度固定,保证在分割的位置不会阶段dom元素,不过这个想法很快放弃,因为项目上需要被生成的dom区域的结构、高度都不固定
    2)不依赖布局,给页面上的子元素添加标识, 根据动态计算子元素所在的高度计算出一个位置插入空白高度

    // html示例:
     <div ref="targetDom">
        <div class="target-node-item">aaaaaa</div>
        <div class="target-node-item">aaaaaa</div>
        <div>
           <div class="target-node-item">bbbbbb</div>
           <div class="target-node-item">bbbbbb</div>
           <div>
              <div class="target-node-item">cccccc</div>
              <div class="target-node-item">cccccc</div>
           </div>
            <div>
              <div class="target-node-item">cccccc</div>
              <div class="target-node-item">cccccc</div>
              <div>
                <div class="target-node-item">ddddd</div>
                <div class="target-node-item">ddddd</div>
              </div>
           </div>
         </div>
      </div>
    

    由于dom结构的大小宽高不固定,所以在不能被截断的元素上添加target-node-item,根据该标识遍历子节点中class为target-node-item的元素,计算出target-node-item的元素距离顶部的偏移量,如果(元素a距离上方或上层控件的位置+元素a本身的高度小于A4纸的高度,并且下一个元素距离上方或上层控件的位置+下一个元素本身的高度大于A4纸的高度),则在两个园中中间插入一个空白块,空白的高度通过计算,为a4纸的高度减去元素a的offsetTop + offsetHeight,我们可以在计算出的高度上插入占位高度,避免下一页内容挨着分割线、
    如果需要页码的话,可以在判断高度的位置预留出页码的高度,在对应的位置插入页码节点。

    isSplit (nodes, index, pageHeight) {
          return nodes[index].offsetTop + nodes[index].offsetHeight < pageHeight && nodes[index + 1] && nodes[index + 1].offsetTop + nodes[index + 1].offsetHeight > pageHeight
     }
    
    /**
    * @description: 获取底部元素
    */
    getFooterElement (remainingHeight, fillingHeight = 85) {
          let newNode = document.createElement('div')
          newNode.style.background = '#ffffff'
          newNode.style.width = 'calc(100% + 8px)'
          newNode.style.marginLeft = '-4px'
          newNode.style.marginBottom = '0px'
          newNode.style.borderTop = '1px solid #7F7F7F'
          newNode.style.height = (remainingHeight + fillingHeight) + 'px' // pdf截断需要一个空白位置
          return newNode
     },
    
    outPutPdfFn () {
          let vm = this
          const A4_WIDTH = 592.28
          const A4_HEIGHT = 841.89
          vm.$nextTick(async () => {
            let target = vm.$refs.targetDom
            let pageHeight = target.scrollWidth / A4_WIDTH * A4_HEIGHT
            let lableListID = document.getElementsByClassName('target-node-item')
            for (let i = 0; i < lableListID.length; i++) {
              let multiple = Math.ceil((lableListID[i].offsetTop + lableListID[i].offsetHeight) / pageHeight)
              if (this.isSplit(lableListID, i, multiple * pageHeight)) {
                let divParent = lableListID[i].parentNode // 获取该div的父节点
                let _H = multiple * pageHeight - (lableListID[i].offsetTop + lableListID[i].offsetHeight)
                let newNode = vm.getFooterElement(_H)
                let next = lableListID[i].nextSibling // 获取div的下一个兄弟节点
                // 判断兄弟节点是否存在
                if (next) {
                  // 存在则将新节点插入到div的下一个兄弟节点之前,即div之后
                  divParent.insertBefore(newNode, next)
                } else {
                  // 不存在则直接添加到最后,appendChild默认添加到divParent的最后
                  divParent.appendChild(newNode)
                }
              }
            }
            vm.$createPdf(target, '文件名', async (file) => {
              // file对象
            })
          })
        }
    

    处理前:


    image.png

    处理后:


    image.png

    五、踩坑点

    1、滚动截屏

    由于html元素过多,页面上可滑动查看,此时 元素width≠元素scrollWidth , 元素height≠元素一开始的想法是把目标元素的width,height赋值为scrollWidth,scrollHeight,但这样会导致页面会闪烁

        targetDom.style.width = targetDom.scrollWidth + 'px'
        targetDom.style.height = targetDom.scrollHeight + 'px'
    

    优化:把目标元素copy下来,html2canvas中引入cloneDom

      let cloneDom = targetDom.cloneNode(true)
      cloneDom.style.width = targetDom.scrollWidth + 'px'
      cloneDom.style.height = targetDom.scrollHeight + 'px'
      cloneDom.style.background = '#FFFFFF'
      document.body.appendChild(cloneDom)
    
    2、截屏区域有跨域图片

    配下参数即可即:

    useCORS: true   允许加载跨域图片
    

    相关文章

      网友评论

        本文标题:vue页面生成pdf且避免分页截断处理

        本文链接:https://www.haomeiwen.com/subject/fnejcktx.html