博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
前台导出pdf经验汇总 (html2canvas.js和浏览器自带的打印功能-print.js)以及后台一些导出pdf的方法
阅读量:4029 次
发布时间:2019-05-24

本文共 15161 字,大约阅读时间需要 50 分钟。

项目导出pdf

前台导出pdf的两种方式

html2canvas.js & pdf.js

简介

我们可以直接在浏览器端使用html2canvas,对整个或局部页面进行‘截图’。但这并不是真的截图,而是通过遍历页面DOM结构,收集所有元素信息及相应样式,渲染出canvas image。

由于html2canvas只能将它能处理的生成canvas image,因此渲染出来的结果并不是100%与原来一致。但它不需要服务器参与,整个图片都由客户端浏览器生成,使用很方便。

使用

先下载html2canvas.jsjspdf插件:

yarn add html2canvasnpm install --save html2canvas yarn add jspdfnpm install jspdf --save

使用的API也很简洁,下面代码可以将某个元素渲染成canvas:

html2canvas(element, {
onrendered: function(canvas) {
document.body.appendChild(canvas); }});

当然了,这是个非常简单的html转成pdf的js方法。

问题及解决方法

如果我们想要输出多页pdf,需要清晰度更高的,同时内容不被截断的改怎么办呢?

  1. 输出多页pdf
// // 导出页面为PDF格式import html2canvas from 'html2canvas'import JSPDF from 'jspdf'export default {
install (Vue, options) {
Vue.prototype.ExportSavePdf = function (htmlTitle, currentTime) {
// 导出之前先将滚动条置顶,不然会出现数据不全的现象 window.pageYOffset = 0 document.documentElement.scrollTop = 0 document.body.scrollTop = 0 var element = document.getElementById('pdfCentent') element.style.background = '#FFFFFF' html2canvas(element, {
logging: false, dpi: window.devicePixelRatio * 4, // 将分辨率提高到特定的DPI 提高四倍 scale: 4, // 按比例增加分辨率 element: element, backgroundColor: '#ffffff', allowTaint: true, useCORS: true }).then(function (canvas) {
var pdf = new JSPDF('p', 'mm', 'a4') // A4纸,纵向 pdf.setFontSize(30) // 字体大小 // pdf.text(20, 30, pdf) var ctx = canvas.getContext('2d') var a4w = 170; var a4h = 257 // A4大小,210mm x 297mm,四边各保留20mm的边距,显示区域170x257 var imgHeight = Math.floor(a4h * canvas.width / a4w) // 按A4显示比例换算一页图像的像素高度 var renderedHeight = 0 var options = {
pagesplit: true } while (renderedHeight < canvas.height) {
var page = document.createElement('canvas') page.width = canvas.width page.height = Math.min(imgHeight, canvas.height - renderedHeight)// 可能内容不足一页 // 用getImageData剪裁指定区域,并画到前面创建的canvas对象中 page.getContext('2d').putImageData(ctx.getImageData(0, renderedHeight, canvas.width, Math.min(imgHeight, canvas.height - renderedHeight)), 0, 0) pdf.addImage(page.toDataURL('image/jpeg', 1.0), 'JPEG', 10, 10, a4w, Math.min(a4h, a4w * page.height / page.width)) // 添加图像到页面,保留10mm边距 renderedHeight += imgHeight if (renderedHeight < canvas.height) {
pdf.addPage(element, options, function () {
}) }// 如果后面还有内容,添加一个空页 // delete page; } pdf.save(htmlTitle + currentTime) }) } }}

2.清晰度更高:

// // 导出页面为PDF格式import html2canvas from 'html2canvas'import JSPDF from 'jspdf'export default {
install (Vue, options) {
Vue.prototype.ExportSavePdf = function (htmlTitle, currentTime) {
// 清晰度更高 var downPdf = document.getElementById('pdfCentent') html2canvas(downPdf, {
logging: false, // dpi: 172, dpi: window.devicePixelRatio * 4, // 将分辨率提高到特定的DPI 提高四倍 scale: 4 // 按比例增加分辨率 }).then(function (canvas) {
console.log('canvas: ', canvas) var contentWidth = canvas.width var contentHeight = canvas.height // 一页pdf显示html页面生成的canvas高度; var pageHeight = contentWidth / 592.28 * 841.89 // 未生成pdf的html页面高度 var leftHeight = contentHeight // pdf页面偏移 var position = 0 // html页面生成的canvas在pdf中图片的宽高(a4纸的尺寸[595.28,841.89]) var imgWidth = 595.28 var imgHeight = 592.28 / contentWidth * contentHeight var pageData = canvas.toDataURL('image/jpeg', 1.0) var pdf = new JSPDF('', 'pt', 'a4') // 有两个高度需要区分,一个是html页面的实际高度,和生成pdf的页面高度(841.89) // 当内容未超过pdf一页显示的范围,无需分页 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(htmlTitle + currentTime) // 背景设为白色(默认为黑色) // background: '#fff' }) } }}

3.内容不被截断,也就是把所有的内容放一页:

// // 导出页面为PDF格式import html2canvas from 'html2canvas'import JSPDF from 'jspdf'export default {
install (Vue, options) {
Vue.prototype.ExportSavePdf = function (htmlTitle, currentTime) {
// -------------输出pdf为一页,解决分页内容截断问题,单在pdf打印时会出错------------- var mainRight = document.getElementById('pdfCentent') html2canvas(mainRight, {
allowTaint: true, scale: 2 // 提升画面质量,但是会增加文件大小 }).then(function (canvas) {
var contentWidth = canvas.width var contentHeight = canvas.height var pageData = canvas.toDataURL('image/jpeg', 0.4) var pdfWidth = (contentWidth + 10) / 2 * 0.75 var pdfHeight = (contentHeight + 200) / 2 * 0.75 // 500为底部留白 var imgWidth = pdfWidth var imgHeight = (contentHeight / 2 * 0.75) // 内容图片这里不需要留白的距离 var pdf = new JSPDF('', 'pt', [pdfWidth, pdfHeight]) pdf.addImage(pageData, 'jpeg', 0, 0, imgWidth, imgHeight) // pdf.save('report_pdf_' + new Date().getTime() + '.pdf') pdf.save(htmlTitle + currentTime)}) } }}

问题:

使用这个插件固然可以将html转换成pdf输出,但是原理是将html先转换成canvas图片,然后将图片转成pdf,这样图片的清晰度不高,同时存在内容被截断的情况,虽然可以通过修改js文件来判断是否截断内容,但是设计的文本内容比较多,例如:表格、文本、图片、图表等,不好判断。
于是采用下面的方法实现前台导出pdf,利用print.js

浏览器自带的打印功能 & print.js

简介

浏览器自带的打印功能也可以输出pdf,图证如下:

在这里插入图片描述

使用

两种引入方式:

1.引入安装vue-print.js

cnpm i vue-printjs --save-dev

2.下载print.js包:

使用方法
1.main.js中引入插件

import Print from './plugins/print/Print'Vue.use(Print)

2.vue项目中使用:

问题及解决方法

这个插件用的人比较少,所以我在项目中遇见的问题还是比较多的,下面就一一解决。程序员就是解决问题的,QAQ

1、如果项目需要多页pdf,那么肯定需要分页,首先确定好,每一页的大小,去适配你的界面,因为每个人的需求不一样,大小也不一样,下面放我设置的大小:

// 报告右侧块样式.reportBlock{
border-radius: 2px; border:1px solid; border-color:#dcdddd; width:731px; height:1024px; clear: both; margin: 0 auto; position: relative; margin-top: 20px; margin-left: 0px; margin-bottom: 20px;}

2、每页大小确定了,那么就需要实现分页了,这里用到一个css样式:

page-break-after: always; // 实现分页的重要一步

整个代码就是:

// 报告右侧块样式.reportBlock{
border-radius: 2px; border:1px solid; border-color:#dcdddd; width:731px; height:1024px; clear: both; margin: 0 auto; position: relative; margin-top: 20px; margin-left: 0px; margin-bottom: 20px; page-break-after: always; // 实现分页的重要一步}

3、既然有了分页,自然也就需要滚动条,总不至于使用浏览器自带的滚动条,那么影响整个系统使用。再者,如果界面内容使用滚动条,出现滚动条也是不美观的,特别对于我们前端开发人员来说,非常碍眼。我们可以利用两个div块来实现:

.rightBlock{
overflow: hidden; width: 840px; height: 750px; }

第一个div块里是空的,设置宽度较小,目的是挡住第二个div的滚动条;

.printBlock{
width: 860px; height: 100%; overflow-y: scroll; overflow-x: hidden;}

第二个div块里放置我们需要展示的内容,设置可滚动,然后设置宽度大于第一div宽度。

这样就实现既有滚动条又隐藏起来啦。
这里要注意不能使用这个css属性:

min-height:666px;max-height:666px;overflowY:scroll;

这个css样式会导致分页失败哟!

4、在弹出打印预览的时候,会发现出现页眉页脚,这就需要去除。使用如下代码:

// 去除页眉页脚#print{
@page {
margin: 0; /* this affects the margin in the printer settings */ } }

这里的print就是对应的你想输出pdf的div块。

5、在出现需要空白的地方,比如第一页,我们第一个想到的是不是加个矩形框,但是事实证明这是失败的。需要使用css外边框属性,类似:margin-top。而不是另外加div框。

6、如果你的项目使用了echarts图表直接浏览器打印显示不出来,因为print.js本身是不支持canvas转成图片的功能的,需要将echarts的转成图片,提前把canvas图表转成图片,这里使用自己写的代码,记得这里的代码是在刚刚引入的main.jsprint.js中:

在这里插入图片描述

var canvass = document.querySelectorAll('canvas');

在这里插入图片描述

//canvass echars图表转为图片    for (var k4 = 0; k4 < canvass.length; k4++) {
var imageURL = canvass[k4].toDataURL("image/png"); var img = document.createElement("img"); img.src = imageURL; img.setAttribute('style', 'max-width: 100%;'); img.className = 'isNeedRemove' // canvass[k4].style.display = 'none' // canvass[k4].parentNode.style.width = '100%' // canvass[k4].parentNode.style.textAlign = 'center' canvass[k4].parentNode.insertBefore(img,canvass[k4].nextElementSibling); }

print.js中还写了分页的函数:

在这里插入图片描述

//做分页    //style="page-break-after: always"    var pages = document.querySelectorAll('.result');    for (var k5 = 0; k5 < pages.length; k5++) {
pages[k5].setAttribute('style', 'page-break-after: always'); } return this.dom.outerHTML;

在这里插入图片描述

writeIframe: function (content) {
var w, doc, iframe = document.createElement('iframe'), f = document.body.appendChild(iframe); iframe.id = "myIframe"; //iframe.style = "position:absolute;width:0;height:0;top:-10px;left:-10px;"; iframe.setAttribute('style', 'position:absolute;width:' + document.querySelector('.results').clientWidth + 'px;height:0;top:-10px;left:-10px;'); w = f.contentWindow || f.contentDocument; doc = f.contentDocument || f.contentWindow.document; doc.open(); doc.write(content); doc.close(); var removes = document.querySelectorAll('.isNeedRemove'); for (var k = 0; k < removes.length; k++) {
removes[k].parentNode.removeChild(removes[k]); } var _this = this iframe.onload = function(){
_this.toPrint(w); setTimeout(function () {
document.body.removeChild(iframe) }, 100) } },

担心你们找不到位置就截图把需要添加的地方标出来,然后代码紧接着放在地下,供你们复制喔。毕竟大家都是面向CV编程嘛,QAQ

但是有的小伙伴说,这一个个找的太费力了,我就想直接CV,好的满足你,下面自取喔:

// 打印类属性、方法定义/* eslint-disable */const Print = function (dom, options) {
if (!(this instanceof Print)) return new Print(dom, options); this.options = this.extend({
'noPrint': '.no-print' }, options); if ((typeof dom) === "string") {
this.dom = document.querySelector(dom); } else {
this.isDOM(dom) this.dom = this.isDOM(dom) ? dom : dom.$el; } this.init();};Print.prototype = {
init: function () {
var content = this.getStyle() + this.getHtml(); this.writeIframe(content); }, extend: function (obj, obj2) {
for (var k in obj2) {
obj[k] = obj2[k]; } return obj; }, getStyle: function () {
var str = "", styles = document.querySelectorAll('style,link'); for (var i = 0; i < styles.length; i++) {
str += styles[i].outerHTML; } str += ""; str += ""; return str; }, getHtml: function () {
var inputs = document.querySelectorAll('input'); var textareas = document.querySelectorAll('textarea'); var selects = document.querySelectorAll('select'); var canvass = document.querySelectorAll('canvas'); for (var k = 0; k < inputs.length; k++) {
if (inputs[k].type == "checkbox" || inputs[k].type == "radio") {
if (inputs[k].checked == true) {
inputs[k].setAttribute('checked', "checked") } else {
inputs[k].removeAttribute('checked') } } else if (inputs[k].type == "text") {
inputs[k].setAttribute('value', inputs[k].value) } else {
inputs[k].setAttribute('value', inputs[k].value) } } for (var k2 = 0; k2 < textareas.length; k2++) {
if (textareas[k2].type == 'textarea') {
textareas[k2].innerHTML = textareas[k2].value } } for (var k3 = 0; k3 < selects.length; k3++) {
if (selects[k3].type == 'select-one') {
var child = selects[k3].children; for (var i in child) {
if (child[i].tagName == 'OPTION') {
if (child[i].selected == true) {
child[i].setAttribute('selected', "selected") } else {
child[i].removeAttribute('selected') } } } } } //canvass echars图表转为图片 for (var k4 = 0; k4 < canvass.length; k4++) {
var imageURL = canvass[k4].toDataURL("image/png"); var img = document.createElement("img"); img.src = imageURL; img.setAttribute('style', 'max-width: 100%;'); img.className = 'isNeedRemove' // canvass[k4].style.display = 'none' // canvass[k4].parentNode.style.width = '100%' // canvass[k4].parentNode.style.textAlign = 'center' canvass[k4].parentNode.insertBefore(img,canvass[k4].nextElementSibling); } //做分页 //style="page-break-after: always" var pages = document.querySelectorAll('.result'); for (var k5 = 0; k5 < pages.length; k5++) {
pages[k5].setAttribute('style', 'page-break-after: always'); } return this.dom.outerHTML; }, writeIframe: function (content) {
var w, doc, iframe = document.createElement('iframe'), f = document.body.appendChild(iframe); iframe.id = "myIframe"; //iframe.style = "position:absolute;width:0;height:0;top:-10px;left:-10px;"; iframe.setAttribute('style', 'position:absolute;width:' + document.querySelector('.results').clientWidth + 'px;height:0;top:-10px;left:-10px;'); w = f.contentWindow || f.contentDocument; doc = f.contentDocument || f.contentWindow.document; doc.open(); doc.write(content); doc.close(); var removes = document.querySelectorAll('.isNeedRemove'); for (var k = 0; k < removes.length; k++) {
removes[k].parentNode.removeChild(removes[k]); } var _this = this iframe.onload = function(){
_this.toPrint(w); setTimeout(function () {
document.body.removeChild(iframe) }, 100) } }, toPrint: function (frameWindow) {
try {
setTimeout(function () {
frameWindow.focus(); try {
if (!frameWindow.document.execCommand('print', false, null)) {
frameWindow.print(); } } catch (e) {
frameWindow.print(); } frameWindow.close(); }, 10); } catch (err) {
console.log('err', err); } }, isDOM: (typeof HTMLElement === 'object') ? function (obj) {
return obj instanceof HTMLElement; } : function (obj) {
return obj && typeof obj === 'object' && obj.nodeType === 1 && typeof obj.nodeName === 'string'; }};const MyPlugin = {
}MyPlugin.install = function (Vue, options) {
// 4. 添加实例方法 Vue.prototype.$print = Print}export default MyPlugin

7、有时候预览的时候会出现中间突然多一页空白页,这可能是你用的组件出了问题,比如说我用的vuecard,封装好的自带内边距padding:24px,这个会导致的。

两个解决办法:
1、如果你的card都想改,或者你引用的某个组件,更改内联样式不影响其他的,你可以使用vue的深度内联deep来更改:

/deep/.ant-transfer-list-header{
background: #01ada8;}

类似这样的,具体更改card的我没用,这个号小问题对于各位看官来说问题不大,自己解决咯。

2、将card改成div就好啦。

总结

前台输出pdf就到这了,可能还有问题没写在里面,以后遇见还会更新。也欢迎大家有问题提问,共同进步。

后台输出pdf

利用iText导出

因为我使用的前台导出,当时也看了后台,因为项目中有很多复杂图表,后台实现不了,所以放弃这个途径。小伙伴在抉择的时候也要注意哦。

下面放上几个当时我看的几篇博客,希望对大家有帮助!
1、
2、
3、
设计到前后台文件流传输的文章:
1、
2、
3、
4、

转载地址:http://iqlbi.baihongyu.com/

你可能感兴趣的文章
C++获取文件大小常用技巧分享
查看>>
未来5年大机遇:做贩卖多巴胺的超级玩家
查看>>
关于AIS编码解码的两个小问题
查看>>
GitHub 万星推荐:黑客成长技术清单
查看>>
可以在线C++编译的工具站点
查看>>
关于无人驾驶的过去、现在以及未来,看这篇文章就够了!
查看>>
所谓的进步和提升,就是完成认知升级
查看>>
昨夜今晨最大八卦终于坐实——人类首次直接探测到了引力波
查看>>
如何优雅、机智地和新公司谈薪水?
查看>>
为什么读了很多书,却学不到什么东西?
查看>>
长文干货:如何轻松应对工作中最棘手的13种场景?
查看>>
如何确保自己的Mac数据安全呢?这里有四个“小秘诀”
查看>>
如何用好碎片化时间,让思维更有效率?
查看>>
第一性原理:戳中问题本质的人是怎么思考的?
查看>>
No.147 - LeetCode1108
查看>>
No.148 - LeetCode771
查看>>
No.174 - LeetCode1305 - 合并两个搜索树
查看>>
No.175 - LeetCode1306
查看>>
No.176 - LeetCode1309
查看>>
No.182 - LeetCode1325 - C指针的魅力
查看>>