vue导出多页PDF截断问题

一、前言

个人网站上线了,欢迎大家访问 苏浩的个人博客

使用的第三方:html2canvasjspdf

为了一劳永逸(更好的偷懒),做了一个简历修改的页面,将简历信息保存到数据库同时使用html2canvasjspdf导出PDF,但是在导出PDF时却发现文本内容在分页部分被直接截断,经过查阅资料没找到匹配的结果,于是就自己想办法解决吧。

二、正文

首先是导出PDF的工具方法,直接修改Vue的原型方便调用

Vue.prototype.getPdf = function(id, title) {
	html2Canvas(document.querySelector(`#${id}`), {
		useCORS: true //看情况选用上面还是下面的,
	}).then(function(canvas) {
		let contentWidth = canvas.width
		let contentHeight = canvas.height
		let pageHeight = contentWidth / 592.28 * 841.89
		let leftHeight = contentHeight
		let position = 0
		let imgWidth = 595.28
		let imgHeight = 592.28 / contentWidth * contentHeight
		let pageData = canvas.toDataURL('image/jpeg', 1.0)
		let PDF = new JsPDF('', '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 -= 841.89
				if (leftHeight > 0) {
					PDF.addPage()
				}
			}
		}
		PDF.save(title + '.pdf')
	})
}

需要打印的结构是这样的,多个子组件

<div id="preview">
		<!-- 头 -->
		<resume-header id="resume-header"></resume-header>
		<!-- 基本信息 -->
		<basic-info :userInfo="resume.userInfo" class="p40" id="resume-info" style="padding-top:40px;"></basic-info>
		<!-- 教育背景 -->
		<education class="p40" id="education"></education>
		<!-- 项目经历 -->
		<project :project="resume.project" class="p40" id="project"></project>
		<!-- 实习经历 -->
		<practice :practice="resume.practice" class="p40" id="practice"></practice>
		<!-- 工作经历 -->
		<work :work="resume.work" class="p40" id="work"></work>
		<!-- 技能 -->
		<skill :skill="resume.skill" class="p40" id="skill"></skill>
		<!-- 自我评价 -->
		<self-introduction :selfIntroduction="resume.selfIntroduction" class="p40" id="self"
			style="padding-bottom:40px;"></self-introduction>
	</div>

1. 最初思路

思路:定义一个DOM容器,遍历所有需要打印的节点,并逐个添加到这个DOM容器内,每次添加之前先判断这个添加进去后是否会超出一页的高度,如果没超出,则添加进去,否则证明快到一页的结尾了,则当前页剩余部分填充一个空白div。

这个思路比较简单,贴一段伪代码吧

function printPDF() {
    let children = document.getElementById("你需要打印的dom节点").children;
    let box = document.createElement("div"); // 存放dom的容器
    const pageHeight = 1500; // 一页的高度
    let height = 0; // box中已经添加节点的总高度
    for(let i=0; i<children.length; i++) {
        let node = children[i];
        if(node.offsetHeight + height < pageHeight) {
            // 添加这个节点仍不会超出一页高度
            height += node.offsetHeight;
            box.appendChild(node.cloneNode(true)); // 这里使用cloneNode时为了防止将原有dom移除
        } else {
            // 加入这个节点后高度超出一页,这页剩余部分填充一个空白div
            let empty = document.createElement("div");
            empty.style.height = pageHeight - height; // 这页剩余高度
            box.appendChild(empty);
            height = 0; // 填充了空白后,这页就结束了,因此重置height
        }
    }
    // 调用导出pdf方法,这里省略
}

2. 改进思路

经过尝试,确实会准确分页,也不会裁断,但是发现个问题,如果有个dom节点很高,几乎占据了一页,哪怕第一页只有少量内容,也会填充一大片空白,看起来很不友好。

原因很简单,因为在循环时,这个节点加进去后会超出一页高度,所以剩余部分默认填充了空白div,也就是上面代码循环中else部分。

改进方法,在else分支中判断剩余节点,看看剩余节点中是否有节点可以放置到当前页中,如果有,则添加到当前页中,如果没有,只能填充空白了(或者将子节点拆分成多个更小的节点也可以)

思路:

第一步:维护一个数组printOrderArr,保存需要往dom容器中追加节点的顺序(包括填充),遍历思路和上面一样,不过改进了超出当前页的判断,如果超出当前页,先在剩余节点中查找是否有其余节点可以添加进来,如果实在没有在填充空白。

第二步:遍历printOrderArr,如果当前遍历对象不是空白填充,则在子节点children中查找对应节点添加到容器中,否则填充空白。

第三步:打印容器中的内容

function exportPDF() {
	let dom = document.getElementById("preview");
	// 获取所有子节点
	let children = dom.children[0].children;
	// 维护一个数组,保存每个子节点的id和高度,代替children进行增删改查操作
    // 由于dom节点数组不是数组,因此通过call进行遍历
	let data = Array.prototype.map.call(children, item => {
		return {
			id: item.id,
			height: item.offsetHeight
		}
	})
	let domHeight = 0;
	// 预计算,根据每个节点的高度生成一个数组,遍历数组将对应dom保存到待打印dom中
	let printOrderArr = [];
	// 头部、个人信息、教育背景优先打印
	for (let i = 0; i < 3; i++) {
		printOrderArr.push({
			id: data[i].id,
			isEmpty: false
		})
		domHeight += data[i].height
	}
	data.splice(0, 3);
	// 遍历所有子节点
	while (data.length) {
		// 每一次处理第一个节点
		const nodeHeight = data[0].height;
		// 判断当前节点是否能加入到数组中
		if ((domHeight + nodeHeight) < this.pageHeight) {
			printOrderArr.push({
				id: data[0].id,
				isEmpty: false
			})
			domHeight += nodeHeight;
			// 删除当前元素
			data.splice(0, 1);
		} else {
			// 当前页剩余空白高度
			const lastHeight = this.pageHeight - domHeight;
			// 保存可以填充空白的板块
			let node;
			for (let j = 0; j < data.length; j++) {
				if (data[j].height < lastHeight) {
					node = data[j];
					data.splice(j, 1);
				}
			}
			// 如果有其他模块可以填充剩余空白部分
			if (node) {
				printOrderArr.push({
					id: node.id,
					isEmpty: false
				});
				domHeight += node.height;
			} else {
				// 剩余所有模块都无法填充剩余的那块空白
				printOrderArr.push({
					height: lastHeight,
					isEmpty: true
				})
                 // 当前页已经填充了空白,重置domHeight,开始下一页
				domHeight = 0;
			}
		}
	}
    // dom节点的容器
	let pdfDom = document.createElement("div");
	pdfDom.id = "pdf";
    // 循环加入到容器中
	for (let i = 0; i < printOrderArr.length; i++) {
		let node = printOrderArr[i];
        // 判断是否是填充的空白div
		if (!node.isEmpty) {
            // 通过id获取到子节点中对应节点,通过遍历查找也是可以的
			let dom = Array.prototype.filter.call(children, item => item.id == node.id)[0];
			pdfDom.appendChild(dom.cloneNode(true));
		} else {
			let empty = document.createElement("div");
			empty.className = "empty";
			empty.style.height = node.height + 'px';
			pdfDom.appendChild(empty);
		}
	}
	let container = document.getElementsByClassName("resume-container")[0];
	container.appendChild(pdfDom);
	this.getPdf("pdf", "测试打印");
	container.removeChild(pdfDom)
},

经过改进后,打印出来的效果好多了,至少不会出现大片的空白了,因为我的节点都是整体存在,才会出现这个情况,如果子节点不是整体,则第一种方法也可以

三、总结

做完这个功能突然意识到这个解决思路好像和leetcode中的一道题很像,逐渐明白算法并不是离我们很远。

程序 = 算法+ 数据结构绝不是书本空谈,这个需求中printOrderArr就是数据结构的体现,思路就是算法,合在一起就是一个程序。前辈们将业务抽离出去,只保留算法,就是为了让我们学明白算法再来更好的写程序,然而往往很多人不愿意了解算法,我亦如此。

  • 3
    点赞
  • 51
    收藏
    觉得还不错? 一键收藏
  • 17
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 17
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值