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

前言

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

难点

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

处理思路

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

官网链接

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

代码展示

  1. 首选需要下载两个依赖插件
js 复制代码
npm install html2canvas --save

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

import html2Canvas from 'html2canvas'

import JsPDF from 'jspdf'
const htmlPdf = {
    getPdf(title, html) {
        html2canvas(html, {
    allowTaint: true,
    useCORS: true,
    dpi: 120, // 图片清晰度问题
    background: '#FFFFFF', //如果指定的div没有设置背景色会默认成黑色,这里是个坑
  }).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, 1, 1).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
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对象。例如,如果你想转换整个页面,可以使用如下代码:

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

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

js 复制代码
var dataURL = canvas.toDataURL("image/png");

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

js 复制代码
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的操作,难点就在于封装导出的方法,里面做截取分页的操作,写的不是很好,希望提出宝贵建议。

相关推荐
weixin_382395238 小时前
为小工厂量身打造:本地部署的物料管理系统带缺料计算
前端·制造
__zRainy__8 小时前
解决pnpm v10+不自动构建
前端·pnpm·工程化
猫猫不是喵喵.9 小时前
Vue3 Props 属性
前端·javascript·vue.js
醉城夜风~10 小时前
CSS元素显示模式(display)
前端·css
AI大模型-小华10 小时前
Codex 三方充值快速入门指南
java·前端·数据库·chatgpt·ai编程·codex·chatgpt pro
做前端的娜娜子13 小时前
同一链接实现 PC Web 与移动 H5 自适应
前端·掘金·金石计划
小帅不太帅13 小时前
架构没变、规模没变,DeepSeek V4 Flash 正式版凭什么暴涨 47 分?
前端·aigc·deepseek
jarvisuni13 小时前
DeepSeekFlash前端依旧拉垮,而且变慢了很多!
前端·javascript·算法
卷福同学14 小时前
AI编程出海第二步:验证关键词能否做站
前端·人工智能·后端