vue通过html2canvas jspdf生成PDF问题全解(水印,分页,截断,多页,黑屏,空白,附源码)

18,482次阅读
没有评论

共计 6055 个字符,预计需要花费 16 分钟才能阅读完成。

前端导出 PDF 的方法不多,常见的就是利用 canvas 画布渲染,再结合 jspdf 导出 PDF 文件,代码也不复杂,网上的代码基本都可以拿来即用。如果不是特别追求完美的情况下,或者导出 PDF 内容单页的话,那么基本上也就满足业务需求了。但是,如果你需要导出 PDF 的内容又多又复杂呢?

目录

1.PDF 常规导出 

2. 问题 1:水印

3. 问题 2:导出黑屏与空白

4. 问题 3:分页与截断

5. 结束语


1.PDF 常规导出 

        刚接触这个需求的时候,lz 想的是能 实现将当前网页内容导出 PDF 下载 即可。因为这也符合常规业务需求逻辑,也没有考虑其他的,因此参考了几篇网上的博文,很快就选定了 vue 项目中利用 html2canvas+jspdf 来实现导出 PDF。

        所以,很快就实现了。在此基础上,还满足了可以动态进行 PDF 分页 dom 节点的划分 , 并且觉得就这?就这?蜜汁自信(不自量力)的 lz 还马上写了一篇博客,下面附上博客地址,内附源码。

vue2 利用 html2canvas+jspdf 动态生成多页 PDF_html2pdf 多页 -CSDN 博客

 如果诸位的导出 pdf 内容很简单,那上面的博文应该大概或许能助尔等一臂之力。导出过程中有其他疑难杂症的咱们接着往下看。

2. 问题 1:水印

        给导出的 PDF 加上水印,这个需求很合理吧?毕竟版权和文件安全意识还是要有的。对于这个需求,想要解决也很简单。对应的依赖如 watermark-dom,但是 lz 在用的时候发现这个水印一旦加上就甩都甩不掉,全局都附带上了,而且在导出的时候,PDF 文件上竟然也没带上水印?wtf?

不信邪的可以试试,也有可能是项目的差异性呢,呵呵哒 …

依赖安装

npm install watermark-dom --save

在你要用到水印的页面引入:

import watermark from "watermark-dom";

export default {
    name:'PDF',
    data() {
        return {compony:"多页 PDF 导出"}
    },
    mounted() {
        // 该水印依赖可用,但是导出文件后不会带上
        this.$nextTick(() => {var waterdom = document.getElementById('pdfinsurancepdf'); 
            var height = waterdom.offsetHeight;
            console.log(waterdom,height)
            watermark.load({
                watermark_id: 'wm_div_id',
                watermark_parent_node:'pdfinsurancepdf',   // 水印插件挂载的父元素 element, 不输入则默认挂在 body 上
                watermark_txt: "PDF 导出",                  // 水印的内容
                watermark_fontsize:'24px',                  // 水印字体大小
                watermark_x_space:100,              // 水印 x 轴间隔
                watermark_y_space:100,      
            })
         })
    },
    destroyed() {watermark.remove();
    },
}

 如果上面的方法不好用,你可以试试这个推荐方法:

通过 js 操作 dom 的方式,创建 vue 自定义指令,来动态的给 dom 元素加上水印。

优点 创建后全局可用,精准性高,指哪打哪,而且导出的 PDF 精准的附带了水印,由于是通过 js 来实现的,可塑性强,可通过直接改 js 文件来二次调整适应需求变化

在 untils 目录下创建 watermark.js 文件:

const globalCanvas = null
const globalWaterMark = null
 
// watermark 样式
let style = `
display: block;
overflow: hidden;
position: absolute;
left: 0;
top: 0;
width: 100%;
height: 100%;
background-repeat: repeat;
pointer-events: none;`
 
const getDataUrl = ({font, fillStyle, textAlign, textBaseline, text, rotate = -20}) => {
  font = font || '25px normal'
  fillStyle = fillStyle || 'rgba(180, 180, 180, 0.2)'
  text = text || ''const canvas = globalCanvas || document.createElement('canvas')
  const ctx = canvas.getContext('2d') // 获取画布上下文
  ctx.rotate((rotate * Math.PI) / 180)
  ctx.font = font
  ctx.fillStyle = fillStyle
  ctx.textAlign = textAlign || 'left'
  ctx.textBaseline = textBaseline || 'middle'
  ctx.fillText(text, canvas.width / 10, canvas.height / 2)
 
  return canvas.toDataURL('image/png', 1) // 第二个参数为质量
}
 
const setWaterMark = (el, binding) => {
  //const parentElement = el.parentElement
  const parentElement = el
  // 获取对应的 canvas 画布相关的 base64 url
  const url = getDataUrl(binding)
  // 创建 waterMark 父元素
  const waterMark = globalWaterMark || document.createElement('div')
  waterMark.className = 'water-mark' // 方便自定义展示结果
  style = `${style}background-image: url(${url});`
  waterMark.setAttribute('style', style)
 
  // 将对应图片的父容器作为定位元素
  parentElement.setAttribute('style', 'position: relative;')
 
  // 将图片元素移动到 waterMark 中
  parentElement.appendChild(waterMark)
}
 
// 监听 DOM 变化
const createObserver = (el, binding) => {const waterMarkEl = el.parentElement.querySelector('.water-mark')
 
  const observer = new MutationObserver((mutationsList) => {if (mutationsList.length) {const { removedNodes, type, target} = mutationsList[0]
      const currStyle = waterMarkEl?waterMarkEl.getAttribute('style'):'';
      // 证明被删除了
      if (removedNodes[0] === waterMarkEl) {observer.disconnect()
        // 重新添加水印,dom 监听
        init(el, { value: binding})
      } else if (type === 'attributes' && target === waterMarkEl && currStyle !== style) {waterMarkEl.setAttribute('style', style)
      }
    }
  })
 
  observer.observe(el.parentElement, {
    childList: true,
    attributes: true,
    subtree: true
  })
}
 
// 初始化
const init = (el, binding) => {
  // 设置水印
  setWaterMark(el, binding.value)
  // 启动监控
  createObserver(el, binding.value)
}
 
// 定义指令配置项
const directives = {inserted(el, binding) {init(el, binding)
  }
}
 
export default {
  name: 'watermark',
  directives
}

main.js 进行全局指令注入:

import waterMark from '@/utils/watermark.js'
Vue.directive('watermark', waterMark.directives)

 后面就可以在你需要导出 PDF 的 dom 上附带指令添加水印即可:

 封面

3. 问题 2:导出黑屏与空白

        原因分析:关于这个问题,lz 是在苹果移动端遇到的,谷歌浏览器和安卓环境下都能正常导出,但是在 ios 移动端导出时就出现了黑屏的情况,而且出现黑屏的这一段 pdf 刚好涉及到多页 pdf,单页的 pdf 却是正常的。网上说各种原因的都有,看的很头痛。直到 lz 无意中看到了一个说法:

tips: 这里 PDF 大小指常规 a4 纸大小,A4 大小,210mm x 297mm

不同主流浏览器以及移动终端针对 canvas 画布的大小有对应的限制,而导出 PDF 原理恰好就是通过将当前网页内容通过 canva 渲染成图片再导出 pdf 的。

安卓端绘制 canvas 大小转化成 PDF,大概是 10 几页

苹果端绘制 canvas 大小转化成 PDF,大概 5~6 页

而 lz 黑屏的地方,正好是要一次性生成 9 页 pdf,按照这个思路一测,果然真相大白

vue 通过 html2canvas jspdf 生成 PDF 问题全解(水印,分页,截断,多页,黑屏,空白,附源码)

解决办法:就是分页节点细化,避免绘制 canvas 画布大小超出限制。

4. 问题 3:分页与截断

        对于将网页内容导出 pdf,出现截断问题算是老生常谈的问题了。对于固定的内容,我们可以指定分页节点,再合并导出一个 PDF 文件,不用考虑分页出现截取的情况。但是对于动态的 dom 内容,无法指定分页节点,我们在一股脑导出,让其自动分页的情况下,就很容易出现文字被截取,表格被截取的情况。

vue 通过 html2canvas jspdf 生成 PDF 问题全解(水印,分页,截断,多页,黑屏,空白,附源码)

网上的解决办法也五花八门,什么限制高度啊,动态计算文字高度啊,动态计算分页节点啊。一看就头大。直到 lz 看到了一篇这样的示例:

vue-pdf2: 纯前端导出版本 2(回炉重制版)已解决分页截断,页眉,页脚,页码,页边距,模糊等情况

根据封装的参数来看,也算是能满足很多常规业务需求了。

/**
 * 生成 pdf(处理多页 pdf 截断问题)
 * @param {Object} param
 * @param {HTMLElement} param.element - 需要转换的 dom 根节点
 * @param {number} [param.contentWidth=550] - 一页 pdf 的内容宽度,0-595
 * @param {number} [param.contentHeight=800] - 一页 pdf 的内容高度,0-842
 * @param {string} [param.outputType='save'] - 生成 pdf 的数据类型,添加了 'file' 类型,其他支持的类型见 http://raw.githack.com/MrRio/jsPDF/master/docs/jsPDF.html#output
 * @param {number} [param.scale=window.devicePixelRatio * 2] - 清晰度控制,canvas 放大倍数, 默认像素比 *2
 * @param {string} [param.direction='p'] - 纸张方向,l 横向,p 竖向,默认 A4 纸张
 * @param {string} [param.fileName='document.pdf'] - pdf 文件名,当 outputType='file' 时候,需要加上.pdf 后缀
 * @param {number} param.baseX - pdf 页内容距页面左边的高度,默认居中显示,为(A4 宽度 - contentWidth) / 2)
 * @param {number} param.baseY - pdf 页内容距页面上边的高度,默认 15px
 * @param {HTMLElement} param.header - 页眉 dom 元素
 * @param {HTMLElement} param.footer - 页脚 dom 元素
 * @param {HTMLElement} param.headerFirst - 第一页的页眉 dom 元素(如果需要指定第一页不同页眉时候再传这个, 高度可以和其他页眉不一样)
 * @param {HTMLElement} param.footerFirst - 第一页页脚 dom 元素
 * @param {string} [param.groupName='pdf-group'] - 给 dom 添加组标识的名字,分组代表要进行分页判断,当前组大于一页则新起一页,否则接着上一页
 * @param {string} [param.itemName='pdf-group-item'] - 给 dom 添加元素标识的名字, 设置了 itemName 代表此元素内容小于一页并且不希望被拆分,子元素也不需要遍历,即手动指定深度终点,优化性能
 * @param {string} [param.editorName='pdf-editor'] - 富文本标识类
 * @param {string} [param.tableSplitName='el-table__row'] - 表格组件内部的深度节点
 * @param {string} [param.splitName='pdf-split-page'] - 强制分页,某些情况下可能想不同元素单独起一页,可以设置这个类名
 * @param {string} [param.isPageMessage=false] - 是否显示当前生成页数状态
 * @param {string} [param.isTransformBaseY=false] - 是否将 baseY 按照比例缩小(一般固定 A4 页边距时候可以用上)
 * @param {Array} [param.potionGroup=[]] - 需要计算位置的元素属性,格式是 data-position='xxx',需要同时在节点上加上 param.itemName,如

* @returns {Promise} 根据 outputType 返回不同的数据类型, 是一个对象 */ export class PdfLoader {...}

测试用例效果:

vue 通过 html2canvas jspdf 生成 PDF 问题全解(水印,分页,截断,多页,黑屏,空白,附源码)

5. 结束语

        如果你要问最终 lz 选择了什么方法来解决?我会告诉你,我选择了交给后端。不可否认,前端是万能的,确实能实现将网页内容导出 PDF,但是话又说回来,经过和产品沟通发现,后端 Java 通过依赖工具包生成的 PDF 竟然效果更好?

        所以,需要复杂的 PDF 导出就让后端来吧!别问,问就是前端和后端的相爱相杀~

        别遇到什么事都自己抗,沟通最重要~

原文地址: vue 通过 html2canvas jspdf 生成 PDF 问题全解(水印,分页,截断,多页,黑屏,空白,附源码)

    正文完
     0
    Yojack
    版权声明:本篇文章由 Yojack 于2024-11-07发表,共计6055字。
    转载说明:
    1 本网站名称:优杰开发笔记
    2 本站永久网址:https://yojack.cn
    3 本网站的文章部分内容可能来源于网络,仅供大家学习与参考,如有侵权,请联系站长进行删除处理。
    4 本站一切资源不代表本站立场,并不代表本站赞同其观点和对其真实性负责。
    5 本站所有内容均可转载及分享, 但请注明出处
    6 我们始终尊重原创作者的版权,所有文章在发布时,均尽可能注明出处与作者。
    7 站长邮箱:laylwenl@gmail.com
    评论(没有评论)