一、前言
使用的第三方:html2canvas
和 jspdf
为了一劳永逸(更好的偷懒),做了一个简历修改的页面,将简历信息保存到数据库同时使用html2canvas
和 jspdf
导出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"); const pageHeight = 1500; let height = 0; 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)); } else { let empty = document.createElement("div"); empty.style.height = pageHeight - height; box.appendChild(empty); height = 0; } } }
|
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; let data = Array.prototype.map.call(children, item => { return { id: item.id, height: item.offsetHeight } }) let domHeight = 0; 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 = 0; } } } let pdfDom = document.createElement("div"); pdfDom.id = "pdf"; for (let i = 0; i < printOrderArr.length; i++) { let node = printOrderArr[i]; if (!node.isEmpty) { 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就是数据结构的体现,思路就是算法,合在一起就是一个程序。前辈们将业务抽离出去,只保留算法,就是为了让我们学明白算法再来更好的写程序,然而往往很多人不愿意了解算法,我亦如此。