前端打印pdf的方法
最近公司有项目需要用到前端打印pdf的功能,我网上找了几种方法,发现要不就是不合适,要不就是有缺陷(万恶的百度算法,导致打开的网页都是爬虫抄的),本人就在原有的基础上进行了优化和修改。
首先,打印pdf的方法总共有2类,1是直接用window.print(),2是用pdf插件。这里因为需求简单,我们就选择不引用pdf插件,直接用window.print()打印。
既然打印,我们肯定是需要局部打印,不能把打印按钮也打印进去,那么print方法的局部打印主要分3种方法(下面都是我网上抄的):
1.通过开始、结束标记(startprint、endprint)来打印
function doPrint() { bdhtml=window.document.body.innerHTML; sprnstr="<!--startprint-->"; //开始打印标识字符串有17个字符 eprnstr="<!--endprint-->"; //结束打印标识字符串 prnhtml=bdhtml.substr(bdhtml.indexOf(sprnstr)+17); //从开始打印标识之后的内容 prnhtml=prnhtml.substring(0,prnhtml.indexOf(eprnstr)); //截取开始标识和结束标识之间的内容 window.document.body.innerHTML=prnhtml; //把需要打印的指定内容赋给body.innerHTML window.print(); //调用浏览器的打印功能打印指定区域 window.document.body.innerHTML=bdhtml;//重新给页面内容赋值; return false; }
2.通过id选择器来替换内容打印,方法类似第一种
function doPrint2(){ if(getExplorer() == "IE"){ pagesetup_null(); } //根据div标签ID拿到div中的局部内容 bdhtml=window.document.body.innerHTML; var jubuData = document.getElementById("printcontent").innerHTML; //把获取的 局部div内容赋给body标签, 相当于重置了 body里的内容 window.document.body.innerHTML= jubuData; //调用打印功能 window.print(); window.document.body.innerHTML=bdhtml;//重新给页面内容赋值; return false; } function pagesetup_null(){ var hkey_root,hkey_path,hkey_key; hkey_root="HKEY_CURRENT_USER"; hkey_path="\\Software\\Microsoft\\Internet Explorer\\PageSetup\\"; try{ var RegWsh = new ActiveXObject("WScript.Shell"); hkey_key="header"; RegWsh.RegWrite(hkey_root+hkey_path+hkey_key,""); hkey_key="footer"; RegWsh.RegWrite(hkey_root+hkey_path+hkey_key,""); }catch(e){} } function getExplorer() { var explorer = window.navigator.userAgent ; //ie if (explorer.indexOf("MSIE") >= 0) { return "IE"; } //firefox else if (explorer.indexOf("Firefox") >= 0) { return "Firefox"; } //Chrome else if(explorer.indexOf("Chrome") >= 0){ return "Chrome"; } //Opera else if(explorer.indexOf("Opera") >= 0){ return "Opera"; } //Safari else if(explorer.indexOf("Safari") >= 0){ return "Safari"; } }
3.通过动态创建iframe来打印
//判断iframe是否存在,不存在则创建iframe var iframe=document.getElementById("print-iframe"); if(!iframe){ var el = document.getElementById("printcontent"); iframe = document.createElement('IFRAME'); var doc = null; iframe.setAttribute("id", "print-iframe"); iframe.setAttribute('style', 'position:absolute;width:0px;height:0px;left:-500px;top:-500px;'); document.body.appendChild(iframe); doc = iframe.contentWindow.document; //这里可以自定义样式 doc.write('<style media="print">@page {size: auto;margin: 0mm;}</style>'); //解决出现页眉页脚和路径的问题 doc.write('<div>' + el.innerHTML + '</div>'); doc.close(); iframe.contentWindow.focus(); } setTimeout(function(){ iframe.contentWindow.print();},50) //解决第一次样式不生效的问题 if (navigator.userAgent.indexOf("MSIE") > 0){ document.body.removeChild(iframe); }
这3种方法前2种,其实都是通过替换标签来解决的,但是这种方法有个弊端,就是现在页面会发生改变,而第3种不会出现这种情况,那么我们就用第3种。但是第三种方法网上贴出的代码也有问题,主要问题和解决方法如下:
1.css样式带不进去。
外部的css是带不进iframe里的,很多人因此选择直接用内联样式,或者直接iframe里也引用css,但是这样做又遇到一个问题,如果用内联会很丑,而且无法用sass和less,如果直接引用css,直接写html还好,如果用vue这种环境写,在本地环境下会直接放在style里,生产环境下会生成不知名的几个css,这个时候,我们就需要取出style和link:
doc = iframe.contentWindow.document; let styles = document.getElementsByTagName('style'); for(let i = 0;i < styles.length;i++) { doc.write(styles[i].outerHTML) } let links = document.getElementsByTagName('link'); for(let i = 0;i < links.length;i++) { doc.write(links[i].outerHTML) }
如果有deep样式,并且style标签用的是scoped,就需要额外获取data-v作为style的选择器,操作如下:
let data = document.getElementById("printContainer").dataset; let dataV = ""; for(let key in data) { if(key.indexOf("v-")!=-1) { dataV = 'data-' + key; } } doc.write(`<div ${dataV}>${_html}</div>`)
2.选择打印的dom的本身会丢失
这个就是乱copy别人代码的毛病了,人家用innterHTML,你就照抄,实际上应该使用outerHTML。
doc.write('<div>' + el.innerHTML + '</div>'); doc.write('<div>' + el.outerHTML+ '</div>'); //就这么简单
3.因为父级选择器,style不起作用
用刚才上面1的方法style确实传进去了,但是因为父级选择器的关系,style还是会不生效,办法也很简单,我们手动给iframe内部加同名父级。
let _html = el.outerHTML; for(let i = 0;i < parentClassNameList.length;i++) { _html = '<div class="' + parentClassNameList[i] + '">' + _html + '</div>' } doc.write('<div>' + _html + '</div>');
parentClassNameList为父类名列表,一层一层上去。
4.canvas内部的像素不会被copy
这个就需要额外copy,这里我直接贴出copy代码
let _iframeCanvas = iframe.contentWindow.document.getElementById(_canvasId); let _currentCanvas = document.getElementById(_canvasId); let _iframeCtx = _iframeCanvas.getContext("2d"); _iframeCtx.drawImage(_currentCanvas,0,0);
好了,最后我们把所有代码放一起封装一下。
print.js:
const print = { isPrint:false, //styles:iframe的style于主页面的是隔离的,需要额外传输 //otherStyle:额外的style,主要用于控制默认打印大小(如:otherStyle:'transform:scale(1.5);transform-origin: top left;',) //parentClassNameList:由于style可能会带父级的选择器,所以这里也需要模拟父级(由内向外,从domID本身开始算) //dataV:css scoped后需要添加的选择器,一般使用了deep修改css,都需要使用这个参数 start:(props)=>{ if(print.isPrint) return; print.isPrint = true; console.log("print") let domID = props.domID; let styles = props.styles; let links = props.links; let otherStyle = props.otherStyle; let parentClassNameList = props.parentClassNameList; let dataV = props.dataV; let delay = 500; if(props.delay) delay = props.delay; if(!dataV) dataV = ""; let beforePrint = props.beforePrint; let printComplete = props.printComplete; let el = document.getElementById(domID); let iframe=document.getElementById("print-iframe"); if(!iframe){ iframe = document.createElement('IFRAME'); iframe.setAttribute("id", "print-iframe"); iframe.setAttribute('style', 'position:absolute;width:0px;height:0px;left:-500px;top:-500px;z-index:1000'); // iframe.setAttribute('style', 'position:absolute;width:1920px;height:1080px;left:0px;top:0px;z-index:1000'); document.body.appendChild(iframe); } let doc = iframe.contentWindow.document; if(styles) { for(let i = 0;i < styles.length;i++) { // doc.write(`<style> ${styles[i].innerHTML} </style>`) doc.write(styles[i].outerHTML) } } if(links) { for(let i = 0;i < links.length;i++) { doc.write(links[i].outerHTML) } } doc.write('<style media="print">@page {size: auto;margin: 0mm;}</style>'); //解决出现页眉页脚和路径的问题 if(parentClassNameList) { let _html = el.outerHTML; for(let i = 0;i < parentClassNameList.length;i++) { _html = `<div class="${parentClassNameList[i]}">${_html}</div>` } doc.write(`<div style="position: absolute;left: 0;top: ${index*pageHeight}px" ${dataV}>${_html}</div>`) } else { doc.write(`<div style="position: absolute;left: 0;top: ${index*pageHeight}px" style="" ${dataV}>${el.outerHTML}</div>`) } doc.close(); if(otherStyle) { let newEl = iframe.contentWindow.document.getElementById(domID); newEl.setAttribute('style', otherStyle); } iframe.contentWindow.focus(); if(beforePrint) { beforePrint(iframe) } setTimeout(function(){ let _title = document.title; document.title = "布匹检验报告" + title; iframe.contentWindow.print(); document.title = _title; document.body.removeChild(iframe); if(printComplete) { printComplete(iframe) } print.isPrint = false; },delay) //解决第一次样式不生效和图片还没加载出来的问题 }, startPages:(props)=>{ if(print.isPrint) return; print.isPrint = true; console.log("print") let list = props.list; let styles = props.styles; let links = props.links; let title = props.title; let delay = 500; if(props.delay) delay = props.delay; let pageHeight = props.pageHeight; let beforePrint = props.beforePrint; let printComplete = props.printComplete; let iframe=document.getElementById("print-iframe"); if(!iframe){ iframe = document.createElement('IFRAME'); iframe.setAttribute("id", "print-iframe"); iframe.setAttribute('style', 'position:absolute;width:0px;height:0px;left:-500px;top:-500px;z-index:1000'); // iframe.setAttribute('style', 'position:absolute;width:1920px;height:1080px;left:0px;top:0px;z-index:1000'); document.body.appendChild(iframe); } let doc = iframe.contentWindow.document; if(styles) { for(let i = 0;i < styles.length;i++) { // doc.write(`<style> ${styles[i].innerHTML} </style>`) doc.write(styles[i].outerHTML) } } if(links) { for(let i = 0;i < links.length;i++) { doc.write(links[i].outerHTML) } } doc.write('<style media="print">@page {size: auto;margin: 0mm;}</style>'); //解决出现页眉页脚和路径的问题 list.forEach((item,index)=>{ let domID = item.domID; let otherStyle = item.otherStyle; let parentClassNameList = item.parentClassNameList; let dataV = item.dataV; if(!dataV) dataV = ""; let el = document.getElementById(domID); if(parentClassNameList) { let _html = el.outerHTML; for(let i = 0;i < parentClassNameList.length;i++) { _html = `<div class="${parentClassNameList[i]}">${_html}</div>` } doc.write(`<div style="position: absolute;left: 0;top: ${index*pageHeight}px" ${dataV}>${_html}</div>`) } else { doc.write(`<div style="position: absolute;left: 0;top: ${index*pageHeight}px" style="" ${dataV}>${el.outerHTML}</div>`) } if(otherStyle) { let newEl = iframe.contentWindow.document.getElementById(domID); newEl.setAttribute('style', otherStyle); } }) doc.close(); iframe.contentWindow.focus(); if(beforePrint) { beforePrint(iframe) } setTimeout(function(){ let _title = document.title; document.title = "布匹检验报告" + title; iframe.contentWindow.print(); document.title = _title; document.body.removeChild(iframe); if(printComplete) { printComplete(iframe) } print.isPrint = false; },delay) } } export default print
可以看到,我这里还加了多页面打印的版本。
使用:
let styles = document.getElementsByTagName('style'); print.start({ domID:'testPrint',//需要打印的domID styles:styles, parentClassNameList:['dashboard','main-content','page'],//需要打印的dom的上层class beforePrint:(iframe)=>{ //如果有canvas打印需求,打开下面代码 // let _canvasId = "canvas";//canvas的id // let _iframeCanvas = iframe.contentWindow.document.getElementById(_canvasId); // let _currentCanvas = document.getElementById(_canvasId); // let _iframeCtx = _iframeCanvas.getContext("2d"); // _iframeCtx.drawImage(_currentCanvas,0,0); } })
let styles = document.getElementsByTagName('style'); let links = document.getElementsByTagName('link'); let data = document.getElementById("printContainer").dataset; let dataV = ""; for(let key in data) { if(key.indexOf("v-")!=-1) { dataV = 'data-' + key; } } let print1 = document.getElementById('print1'); print.startPages({ styles:styles, links:links, list:[ { domID:"print1", //这里的额外样式是用来缩放页面,使页面刚好撑开到A4纸的大小 otherStyle:`height:${print1.offsetHeight*1.82};transform:scale(${1.82});transform-origin: top left;`, parentClassNameList:['real-page','print-page','summary','his-right','his-container','comp'], dataV:dataV, }, { domID:"print2", parentClassNameList:['real-page','print-page','details','his-right','his-container','comp'], dataV:dataV, } ], beforePrint:(iframe)=>{ let _canvasId = olaHisMap.value.canvasID; let _iframeCanvas = iframe.contentWindow.document.getElementById(_canvasId); let _currentCanvas = document.getElementById(_canvasId); let _iframeCtx = _iframeCanvas.getContext("2d"); _iframeCtx.drawImage(_currentCanvas,0,0); } })
注意:因为缩放功能是用scale实现的,多页面会造成排版遮盖,所以额外样式需要垫高页面。
最后说一下,这个做法vue和react都适用,但是ID需要自己额外赋一下。
发表评论