背景介绍

某老人院信息管理系统项目,甲方要求将财务模块的各种报表导出为PDF文档,方便打印。

之前的解决方案,是将报表生成专门的打印 HTML 页面,然后按 Ctrl+P 调用浏览器本身打印功能去打印。 这种方式存在的问题是不同分辨率的显示器,页面效果不一,需要专门设定打印尺寸,使用起来不够方便,功能上线后,一直遭到甲方吐槽...

轮子工具选择

目标很明确明确,将 HTML 内容导出为PDF。 时间有限,先找轮子,一通谷歌后选定了前端工具 jspdf。具体使用方式比较简单,


解决方案解析

先上代码:

html2canvas(document.body, {
  onrendered:function(canvas) {
    // 要输出的 PDF 每页的宽高尺寸,单位是 pt
    let pageWidth = 841.89
    let pageHeight = 592.28

    // 要打印内容,转换成 canvas 图片后的宽高尺寸
    let contentWidth =  canvas.width*3/4
    let contentHeight = canvas.height*3/4

    // 将要打印内容的图片,等比例缩放至宽度等于输出时 PDF 每页的宽度,此时的图片宽
    let imgWidth = pageWidth
    // 将要打印内容的图片,等比例缩放至宽度等于输出时 PDF 每页的宽度,此时的图片高
    let imgHeight = pageWidth / contentWidth * contentHeight

    // 起始内容截取位置
    let position = 0
    // 剩余未打印内容的高度
    let leftHeight = imgHeight

    // 获取打印内容 canvas 图片元素
    let pageData = canvas.toDataURL('image/jpeg', 1.0)
    
    // 初始化 pdf 容器,三个参数分别是:纸张方向(填'',则是横向)、打印单位、纸张尺寸
    let PDF = new JsPDF('landscape', 'pt', 'a4')
    
    // 循环截取打印内容并添加进容器
    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 -= pageHeight
        if (leftHeight > 0) {
          PDF.addPage()
        }
      }
    }
    
    // 将容器中的内容输出为 PDF 文档
    pdf.save('content.pdf');
  }
})
复制代码

导出 pdf 的函数我参考了这条github链接,做了部分修改。函数逻辑比较简单,不做过多解释。主要提两点:

  1. 修复了一个小bug,原函数忽略了单位转换问题(px 要转 pt),存在导出的 PDF 最后会有空白页。
  2. 原函数中 leftHeight 用的是 contentHeight,也就是 canvas 图片的未缩放换算前的高。这就导致 pageHeight 需要再换算才能得到,这增加了函数逻辑复杂度。其实 leftHeight 可以设为 imgHeight,即缩放换算后的高,而pageHeight 就设为 PDF 单页的高,这样代码逻辑更清晰。

这函数核心逻辑就三步:

  1. 获取要打印内容区域的宽高,并等比缩放直至其宽度等于输出 PDF 的页面的宽度,以此获得缩放后的打印内容图片宽高(imgWidth, imageHeight)
  2. 按单页 PDF 的宽高 (pageWidth, pageHeight),循环截取缩放后的打印内容图片,并将每次截取的内容添加至 PDF 对象容器。(每截取一次,就是一页 pdf)
  3. 将 PDF 对象容器中的内容,输出为 PDF 文档。
问题与补救思路

实践中遇到的问题是竖直方向上图片被随机截断

针对这个项目的业务场景,我采取的补救方案是“设定打印内容高度”。 具体思路如下:

  1. 确定输出单页纸张的尺寸比例
    例如: A4纸宽高比 = 841.89 / 592.28 (横向);
  2. 保持比例不变,通过简单换算确定单个打印的页面宽高
    例如:页面宽 1920px ,高 1360px;
  3. 通过 CSS,精确控制打印页面中各元素的高度,使得超出单页高度的内容,合理过度。
    例如:我想每页打印表格不超过 34 行,那么单行高度就应该设定为 1360/34 = 40px;
    第一页由于有标题、表头等元素,所以只打 30 行,标题和表头合计高度为 160px (这个可根据实际需求,只需保证标题、表头的高度都是单行高的整数倍即可)

最终成功解决竖直方向不规则截断问题。



总结

做的好的三个点:

  1. 快速寻找轮子,思路正确
  2. 知其所以然的态度,促使深入思考实现原理,因此才有可能优化解决方案,使得最终交付的结果更优质
  3. 实在想不到...

不足的两点:

  1. 整个任务完成花费了一个工作日,效率太低。在项目本地部署环节浪费很多时间(主要因为项目本身技术栈选型不好,也没有相应的部署说明文档)。另外,一开始未看懂函数就开始瞎改,浪费了不少时间。
  2. 功能没有合理封装,最终手动复制黏贴到所有页面,浪费大量时间。

进一步研究和思考:

  1. 研究 jspdf 源码,更进一步了解导出 PDF 的实现原理
  2. 研究通用打印页面方案,看能否将导出 PDF 文档功能封装为 vue 组件(先针对打印内容为表格的)