VUE导出多页PDF截断问题

一、前言

使用的第三方:html2canvasjspdf

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

二、正文

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
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')
})
}

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<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。

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
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中查找对应节点添加到容器中,否则填充空白。

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
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就是数据结构的体现,思路就是算法,合在一起就是一个程序。前辈们将业务抽离出去,只保留算法,就是为了让我们学明白算法再来更好的写程序,然而往往很多人不愿意了解算法,我亦如此。