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

3,836 阅读5分钟

前言

这两天公司让做一个需求,前端有一个页面,类似于报告页,需要做导出操作,导出来的文件为PDF格式。另外还有一个需求就是,让报告先上传,再导出下载。于是我查阅了一些资料,总结了一下如何完成,分享一下。

难点

  • 跨域问题:由于浏览器的安全策略,HTML2Canvas 在跨域场景下可能会受到限制。如果要捕获或渲染来自不同域的内容,可能需要通过 CORS(跨域资源共享)或代理服务器来解决跨域问题。
  • 对复杂页面的支持:HTML2Canvas 非常适用于简单的静态页面,但对于复杂的动态页面,如包含大量 JavaScript 动画或通过 AJAX 更新的内容,它可能无法完全捕获和呈现所有元素。
  • 包含外部资源:如果 HTML 元素引用了外部资源,例如图片、字体或 CSS 样式文件等,HTML2Canvas 可能不能直接捕获这些资源并合成成图像。需要特殊处理来确保这些资源正确加载和呈现。
  • 存在图片、组件、文字、表格被分割的现象,需要分页处理

处理思路

  • 监听图片加载事件:使用 JavaScript 监听图片加载事件,确保图片完全加载后再进行截图操作,可以通过添加 onload 事件对图片进行处理。
  • 手动处理动态和异步内容:当页面中有动态或异步获取的内容时,可以在相关操作完成后手动调用html2canvas来实现截图操作。
  • 调整CSS样式:根据html2canvas的支持情况,调整CSS样式以适应截图需求,并考虑使用其他工具或技术来增强效果。

官网链接

官方文档链接 addImage - Documentation (artskydj.github.io)

代码展示

  1. 首选需要下载两个依赖插件
npm install html2canvas --save

npm install jsPDF --save
  1. 接着写一个js文件,导出所封装的方法,具体方法如下
// 页面导出为pdf格式 //title表示为下载的标题,html表示document.querySelector('#myPrintHtml')

import html2Canvas from 'html2canvas'

import JsPDF from 'jspdf'
const htmlPdf = {
    getPdf(title, html) {
        html2canvas(html, {
    allowTainttrue,
    useCORStrue,
    dpi: window.devicePixelRatio * 4, // 将分辨率提高到特定的DPI 提高四倍
    background'#FFFFFF', 
  }).then(canvas => {
    //未生成pdf的html页面高度
    var leftHeight = canvas.height

    var a4Width = 595.28
    var a4Height = 841.89 //A4大小,210mm x 297mm,四边各保留10mm的边距,显示区域190x277
    //一页pdf显示html页面生成的canvas高度;
    var a4HeightRef = Math.floor((canvas.width / a4Width) * a4Height)

    //pdf页面偏移
    var position = 0

    var pageData = canvas.toDataURL('image/jpeg'1.0)

    var pdf = new jsPDF('p''pt''a4'//A4纸,纵向
    var index = 1,
      canvas1 = document.createElement('canvas'),
      height
    pdf.setDisplayMode('fullwidth''continuous''FullScreen')

    var pdfName = title
    function createImpl(canvas) {
      console.log(leftHeight, a4HeightRef)
      if (leftHeight > 0) {
        index++

        var checkCount = 0
        if (leftHeight > a4HeightRef) {
          var i = position + a4HeightRef
          for (i = position + a4HeightRef; i >= position; i--) {
            var isWrite = true
            for (var j = 0; j < canvas.width; j++) {
              var c = canvas.getContext('2d').getImageData(j, i, 11).data

              if (c[0] != 0xff || c[1] != 0xff || c[2] != 0xff) {
                isWrite = false
                break
              }
            }
            if (isWrite) {
              checkCount++
              if (checkCount >= 10) {
                break
              }
            } else {
              checkCount = 0
            }
          }
          height = Math.round(i - position) || Math.min(leftHeight, a4HeightRef)
          if (height <= 0) {
            height = a4HeightRef
          }
        } else {
          height = leftHeight
        }

        canvas1.width = canvas.width
        canvas1.height = height

        console.log(index, 'height:', height, 'pos', position)

        var ctx = canvas1.getContext('2d')
        ctx.drawImage(
          canvas,
          0,
          position,
          canvas.width,
          height,
          0,
          0,
          canvas.width,
          height,
        )

        var pageHeight = Math.round((a4Width / canvas.width) * height)
        // pdf.setPageSize(null, pageHeight)
        if (position != 0) {
          pdf.addPage()
        }
        pdf.addImage(
          canvas1.toDataURL('image/jpeg'1.0),
          'JPEG',
          10,
          10,
          a4Width,
          (a4Width / canvas1.width) * height,
        )
        leftHeight -= height
        position += height
        if (leftHeight > 0) {
          setTimeout(createImpl, 500, canvas)
        } else {
          pdf.save(title + '.pdf')
        }
      }
    }

    //当内容未超过pdf一页显示的范围,无需分页
    if (leftHeight < a4HeightRef) {
      pdf.addImage(
        pageData,
        'JPEG',
        0,
        0,
        a4Width,
        (a4Width / canvas.width) * leftHeight,
      )
      pdf.save(title + '.pdf')
    } else {
      try {
        pdf.deletePage(0)
        setTimeout(createImpl, 500, canvas)
      } catch (err) {
        // console.log(err);
      }
    }
  })

    }
}

export default htmlPdf

  1. 在vue文件中引入所封装的js
import htmlPdf from './html2pdf'   // 这个根据自己所写的文件位置来引入

// 比如写了一个下载的方法 handleDownLoad

handleDownLoad(){
      htmlPdf.getPdf('导出pdf名字', document.querySelector('#print-zone'))
}

// #print-zone 是你想导出的dom节点
// 注意的是,页面有display:none; 的是无法导出来的。

到此位置,一个页面导出PDF就写好了。

下面讲,如何将文件先上传再下载。

文件先上传后导出下载

分析

在我们用插件的时候,为什么可以导出下载,实际上也是因为插件本身将文件先转化为了流,然后才能下载。那么顺着这个思路去写上传,也就是先将文件转化为流,再转化为二进制,再通过formData格式走上传接口,拿到返回的url。

要使用html2canvas将文件转换为二进制流,可以按照以下步骤进行操作:

首先,确保你已经引入了html2canvas库文件到你的项目中。

使用html2canvas库,将需要转换的HTML元素渲染为canvas对象。例如,如果你想转换整个页面,可以使用如下代码:

html2canvas(document.body).then(function(canvas) {
  // 在这里执行后续操作
});

完成渲染后,可以使用canvas对象的toDataURL()方法将图片转换为Base64编码的数据URL。

var dataURL = canvas.toDataURL("image/png");

最后,从Base64编码的数据URL中提取出二进制数据流。可以使用window.atob()和Blob构造函数来实现这一点。

var base64Data = dataURL.split(',')[1]; // 忽略前缀部分 'data:image/png;base64,'
var binaryData = window.atob(base64Data);
var arrayBuffer = new ArrayBuffer(binaryData.length);
var uintArray = new Uint8Array(arrayBuffer);

for (var i = 0; i < binaryData.length; i++) {
  uintArray[i] = binaryData.charCodeAt(i);
}

var blob = new Blob([uintArray], {type: 'image/png'});
let formData = new formData()
formData.append('file',blob)
// 将 formData 放到接口里面传进去 ,在上传接口返回里面,
// 如果上传成功,则执行刚刚导出pdf的方法,这样就完成了一个先上传后下载的需求

现在,你可以使用blob变量来处理这个二进制数据流,比如上传到服务器或者进行其他操作。

请注意,这只是一个基本的示例,具体在你的项目中可能需要根据需求进行适当的修改和调整。

总结

整个导出PDF的操作,难点就在于封装导出的方法,里面做截取分页的操作,写的不是很好,希望提出宝贵建议。