vue基于 html2canvas + jspdf 导出页面为PDF格式

作者: bkbtp 分类: 前端 发布时间: 2018-06-23 16:53
大致原理:将页面转换成图片格式,然后通过图片的base64码生成PDF。

直接上干货,以输出A4纸大小为例

添加两个模块

npm install html2canvas -S
npm install jspdf -S

添加实例方法

import html2Canvas from 'html2canvas'
import JsPDF from 'jspdf'

export default {
  install(Vue, options) {
    Vue.prototype.$getPdf = function (id, title) {
      // 获取当前浏览器滚动条的宽度,原理是设置一个不可见的div,查看设置scorll前后的宽度差
      function getScrollWidth() {
        var noScroll, scroll
        var oDiv = document.createElement('DIV')
        oDiv.style.cssText = 'position:absolute; top:-1000px; width:100px; height:100px; overflow:hidden;'
        noScroll = document.body.appendChild(oDiv).clientWidth
        oDiv.style.overflowY = 'scroll'
        scroll = oDiv.clientWidth
        document.body.removeChild(oDiv)
        return noScroll - scroll
      }

      const SIZE = [595.28, 841.89] // a4宽高
      let node = document.querySelector(`#${id}`)
      let nodeW
      if (getScrollWidth()) {
        nodeW = node.clientWidth - (17 - getScrollWidth())
      } else {
        nodeW = node.clientWidth
      }
      // let nodeW = node.clientWidth
      // 单页高度
      let pageH = nodeW / SIZE[0] * SIZE[1]
      let modules = node.children
      for (let i = 0, len = modules.length; i < len; i++) {
        let item = modules[i]
        let beforeH = item.offsetTop
        let afterH = beforeH + item.clientHeight
        let currentPage = parseInt(beforeH / pageH)
        // div距离父级的高度是pageH的倍数x,但是加上自身高度之后是pageH的倍数x+1,说明被切割
        if (currentPage !== parseInt(afterH / pageH)) {
          // 上一个元素底部距离父级的高度
          let lastItemAftarH = modules[i - 1].offsetTop + modules[i - 1].clientHeight
          let fill = pageH - lastItemAftarH % pageH
          item.style.marginTop = fill + 'px'
        }
      }
      html2Canvas(node, {
        // allowTaint: true,
        useCORS: true, // allowTaint与useCORS看情况二选一,设置 useCORS 为 true,即可开启图片跨域
        scale: 2 // 设置 scale 为 2 及以上,即可支持高分屏
      }).then(function (canvas) {
        let contentWidth = canvas.width
        let contentHeight = canvas.height
        // 一页pdf显示html页面生成的canvas高度
        let pageHeight = contentWidth / SIZE[0] * SIZE[1]
        // 未生成pdf的html页面高度
        let leftHeight = contentHeight
        // pdf页面竖向偏移
        let position = 0
        // 横向页边距
        let sidesway = 0
        // html页面生成的canvas在pdf中图片的宽高
        let imgWidth = SIZE[0] - sidesway * 2
        let imgHeight = imgWidth / contentWidth * contentHeight
        let pageData = canvas.toDataURL('image/jpeg', 1.0)
        let PDF = new JsPDF('', 'pt', 'a4')
        if (leftHeight < pageHeight) {
          PDF.addImage(pageData, 'JPEG', sidesway, position, imgWidth, imgHeight)
        } else {
          while (leftHeight > 0) {
            PDF.addImage(pageData, 'JPEG', sidesway, position, imgWidth, imgHeight)
            leftHeight -= pageHeight
            position -= SIZE[1]
            if (leftHeight > 0) {
              PDF.addPage()
            }
          }
        }
        PDF.save(title + '.pdf')
      })
    }
  }
}


在main.js中使用我们定义的函数文件

import htmlToPdf from '@/utils/htmlToPdf'

Vue.use(htmlToPdf)

在需要导出的页面,调用getPdf函数即可

// 这里是导出页面中某个div内容
<div id="exportBox">
</div>

<el-button type="primary"
           @click="handleExport">导出</el-button>
handleExport() {
  this.$getPdf('exportBox', this.realName)
}

注意事项
  1. 图片不显示
html2Canvas(node, {
  useCORS: true, // 设置 useCORS 为 true,即可开启图片跨域
}).then(function (canvas) {})
  1. 清晰度问题
html2Canvas(node, {
  scale: 2 // 设置 scale 为 2 及以上,即可支持高分屏
}).then(function (canvas) {})
  1. 解决分页异常的问题,比如文字被截断
    这个问题是最需要解决的,下面给出思路:
      将node对象下的每一个一级子元素都当作一个不可被分割的整体,遍历这些子元素,如果某个元素距离body的高度是pageH的倍数x,当加上该元素自身的高度时是pageH的倍数x+1,那么说明x这一页放不下该元素,该元素需要被放在x+1页上,利用paddingTop设置一个留白区域或者直接给该元素设置合适的marginTop即可。
// 获取当前浏览器滚动条的宽度,原理是设置一个不可见的div,查看设置scorll前后的宽度差
function getScrollWidth() {
  var noScroll, scroll
  var oDiv = document.createElement('DIV')
  oDiv.style.cssText = 'position:absolute; top:-1000px; width:100px; height:100px; overflow:hidden;'
  noScroll = document.body.appendChild(oDiv).clientWidth
  oDiv.style.overflowY = 'scroll'
  scroll = oDiv.clientWidth
  document.body.removeChild(oDiv)
  return noScroll - scroll
}

const SIZE = [595.28, 841.89] // a4宽高
let node = document.querySelector(`#${id}`)
let nodeW
if (getScrollWidth()) {
  nodeW = node.clientWidth - (17 - getScrollWidth())
} else {
  nodeW = node.clientWidth
}
// let nodeW = node.clientWidth
// 单页高度
let pageH = nodeW / SIZE[0] * SIZE[1]
let modules = node.children
for (let i = 0, len = modules.length; i < len; i++) {
  let item = modules[i]
  let beforeH = item.offsetTop
  let afterH = beforeH + item.clientHeight
  let currentPage = parseInt(beforeH / pageH)
  // div距离父级的高度是pageH的倍数x,但是加上自身高度之后是pageH的倍数x+1,说明被切割
  if (currentPage !== parseInt(afterH / pageH)) {
    // 上一个元素底部距离父级的高度
    let lastItemAftarH = modules[i - 1].offsetTop + modules[i - 1].clientHeight
    let fill = pageH - lastItemAftarH % pageH
    item.style.marginTop = fill + 'px'
  }
}

这里, getScrollWidth 获取当前浏览器滚动条的宽度和默认宽度17进行比对,是因为我这里给chrome浏览器设置了特殊的滚动条样式 ::-webkit-scrollbar ,但是canvas并不支持此属性,所以canvas获取的宽度还是基于默认浏览器滚动条宽度来计算的。我这里需要打印的区域是自适应浏览器宽度的大小,如果打印区域是固定宽度的,那么就可以移除这部分代码。

后记:
 解决分页的代码中有几个需要注意的小细节:
1、获取子元素的时候,childNodeschildren 有一定的区别。
  childNodes:获取节点,不同浏览器表现不同:
    IE:只获取元素节点;
    非IE:获取元素节点与文本节点;
  children:获取元素节点,浏览器表现相同。
  因此建议使用children。
2、无定位父元素时 offsetParent 为body,offsetTop 计算距离从html开始,所以在这个场景里面需要给父元素加一个相对定位,否则计算会不准确。
3、在解决分页bug的处理中,如果给页面添加占位div或者设置了额外的margin值之类的,会导致html页面产生变化,这里可以刷新一下页面避免多次打印造成计算误差。(刷新页面的方法可以参考vue项目优雅地刷新当前页面,这里不具体展开)
4、上面提到的浏览器滚动条误差等canvas不支持的css属性需要特殊处理

解决了以上几个坑,基本就可以在项目中愉快地使用了,美滋滋!

发表评论

电子邮件地址不会被公开。 必填项已用*标注

知识共享许可协议
本作品采用知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议进行许可。