1.浏览器自身打印
- 使用
window.print()
调起浏览器自带的打印预览弹框打印 - 默认会打印
body
里面所有内容
const handlePrintPdf = () => {
window.print();
}
如果想要局部打印可以使用 CSS
媒体查询,甚至还可以自定义打印时的样式,这些样式只会在打印的时候生效
<!-- 方式1 -->
<link rel="stylesheet" href="./print.css" media="print">
<!-- 方式2 -->
<style media="print"></style>
<!-- 方式3 -->
<style>
@media print {}
</style>
<!-- 方式4 -->
<style>
@import url("./print.css") print;
</style>
可使用媒体查询在打印的时候去除页眉页脚,设置打印布局等操作
@media print {
@page {
/* 去除页眉 */
/* margin-top: 0; */
/* 去除页脚 */
/* margin-bottom: 0; */
/* 去掉页眉和页脚 */
margin: 0;
/* 纵向 */
size: portrait;
/* 横向 A4 */
size:A4 landscape;
}
body {
margin: 1cm;
}
}
如果内容固定且具有分页,浏览器自身分页可能会和预想中情况不一样,我们可以使用 CSS
进行强制分页
@media print {
/* 在main元素前始终插入分页符,强制使其分到下一页 */
main {
page-break-before: always;
}
/* 在footer元素后始终插入分页符 */
footer {
page-break-after: always;
}
}
打印部分区域的内容还可以这样做
动态创建一个不可见的 iframe
, 将需要打印的 dom
节点插入 iframe
内,并调用 iframe
的 print
方法
printBtn.addEventListener("click", () => {
const printContentHtml = document.getElementById("print").innerHTML;
const iframe = document.createElement("iframe");
iframe.setAttribute(
"style",
"position:absolute;width:0px;height:0px;left:-500px;top:-500px;"
);
document.body.appendChild(iframe);
iframe.contentDocument.write(printContentHtml);
iframe.contentDocument.close();
iframe.contentWindow.print();
document.body.removeChild(iframe);
})
或者在新打开的页面打印
printBtn.addEventListener("click", () => {
const printContentHtml = document.getElementById("print").innerHTML;
const printPage = window.open();
printPage.document.write(printContentHtml);
printPage.document.close();
printPage.print();
printPage.close();
})
当然我们也可以这样,先获取需要打印的 dom
节点,替换当前 body
下的节点。完成打印后恢复 body
下的节点
<body>
<p class="para1">段落1</p>
<p class="para2">段落2</p>
<p class="para3">段落3</p>
<button id="printBtn">打印</button>
<script>
printBtn.addEventListener("click", () => {
// 缓存页面内容
const bodyHtml = document.body.innerHTML;
// 获取打印内容
const printHtml = document.querySelector(".para2").innerHTML;
// 替换body内容
document.body.innerHTML = printHtml;
// 打印
window.print();
// 还原页面内容
document.body.innerHTML = bodyHtml;
});
</script>
</body>
这样的话打印的时候页面原有元素会丢失,并且这时可能图片也未加载完成,很多元素宽度也会有问题要调整,我们可以更进一步封装方便修改,整体代码如下:
<body>
<p class="para1">段落1~</p>
<p class="para2">段落2~</p>
<p class="para3">段落3~</p>
<button id="printBtn">打印</button>
<script>
printBtn.addEventListener("click", () => {
// 获取打印的局部内容
const para2Content = document.querySelector(".para2").innerHTML;
printHtml(para2Content);
});
function printHtml(html) {
let printContainer = getContainer(html);
let printStyle = getStyle();
// 添加到页面
document.body.appendChild(printStyle);
document.body.appendChild(printContainer);
// 等待页面图片加载完成开始打印,并在打印完成之后移除旧ODM
LoadedImgsPromise(printContainer).then(() => {
window.print();
document.body.removeChild(printStyle);
document.body.removeChild(printContainer);
});
}
// 新建DOM,将需要打印的内容填充到DOM
function getContainer(html) {
cleanBeforePrintDom();
let container = document.createElement("div");
container.setAttribute("id", "print-container");
container.innerHTML = html;
return container;
}
// 创建新的打印DOM前先移除之前可能存在的旧DOM
function cleanBeforePrintDom() {
let printContainer = document.getElementById("print-container");
if (printContainer) document.body.removeChild(printContainer);
}
// 设置打印样式
function getStyle() {
let styleContent = `
#print-container {
display: none;
}
@media print {
body > :not(.print-container) {
display: none;
}
html,
body {
display: block;
}
#print-container {
display: block;
}
}`;
let style = document.createElement("style");
style.innerHTML = styleContent;
return style;
}
// 图片完整加载
function LoadedImgsPromise(element) {
const imgs = element.querySelectorAll("img");
if (imgs.length === 0) return Promise.resolve();
let loadedImgCount = 0;
return new Promise((resolve) => {
const checkimgCount = () => {
loadedImgCount++;
if (loadedImgCount === imgs.length) {
resolve();
}
};
imgs.forEach(() => {
img.addEventListener("load", checkimgCount);
img.addEventListener("error", checkimgCount);
});
});
}
</script>
</body>
很可惜此类方案和很多第三方库会有元素丢失和样式错乱的情况,不尽如人意
此时一个方案浮出水面
2.html2canvas和jspdf
- 简单说就是将网页通过
html2canvas
(dom-to-image
也可以) 转换为图片,再由jspdf
生成该图片的pdf
- 此方案也会有一些问题,例如
iframe
嵌套(项目中富文本编辑器会用到)的内容会无法打印,无法复制pdf
上的文字等内容,而且清晰度可能会存在问题 - 图片输出
pdf
的时候根据宽高裁切,分页的时候可能会有内容生生的被截断,需要根据不同项目特殊处理(逻辑复杂)
先来说说这两者基本使用
安装:
yarn add html2canvas jspdf
使用 html2canvas
对页面截图
// allowTaint 是否允许跨域加载图片
// useCORS 是否使用CORS从服务器加载图片
// scale 渲染像素比率,默认当前设备像素比
printBtn.addEventListener("click", () => {
html2Canvas(wrap, { allowTaint: true, useCORS: true, scale: 2 }).then(
(canvas) => {
document.body.appendChild(canvas);
// 转换canvase为base64图片,第二个参数1代表图片质量(0-1)
canvas.toDataURL("image/jpeg", 1);
}
);
});
此方法无法打印 iframe
里面的内容,于是我遍历整个 iframe
的 dom
替换,此外我们还要书写逻辑手动对图片指定宽高进行截图分页,此部分可以将其封装下方便复用,具体方案如下
<script setup lang="ts">
import HelloWorld from "./components/HelloWorld.vue";
import html2Canvas from "html2canvas";
import jsPdf from "jspdf";
import domtoimage from "dom-to-image";
async function htmlToPdf(selector) {
// 将html dom节点生成canvas
let htmlCanvas = await getCanvasByselector(selector);
// 将canvas对象转为pdf
let pdf = canvasToPdf(htmlCanvas);
// 通过浏览器下载pdf
await downPdf(pdf, "我的pdf");
}
async function getCanvasByselector(selector) {
let printDom = document.querySelector(selector);
printDom.querySelectorAll("iframe").forEach((iframe) => {
// 创建文档碎片和新的div
let $fragment = document.createDocumentFragment();
let newDiv = document.createElement("p");
// ....中间可以设置newDiv属性和样式等....
// 拿到 iframe 里面每个元素复制
Array.from(iframe.contentWindow.document.body.children).forEach((ele) => {
$fragment.appendChild(ele);
});
newDiv.appendChild($fragment);
// 插入newDiv,内容和iframe一致
iframe.parentNode.insertBefore(newDiv, iframe);
// 移除iframe
iframe.remove();
});
// allowTaint: true 表示允许跨越的图片加载
// useCORS 是否尝试使用 CORS 从服务器加载图片
// scale 渲染的像素比率,默认为当前设备的设备像素比
let canvas = await html2Canvas(printDom, {
allowTaint: true,
useCORS: true,
scale: 1,
});
return canvas;
}
function canvasToPdf(canvas) {
// a4纸的正常尺寸是宽592.28,高是841.89
const pageWidth = 592.28;
const pageHeight = 841.89;
// 设置内容的宽高
const contentWidth = canvas.width;
const contentHeight = canvas.height;
// 默认的偏移量
let position = 0;
// 设置生成图片的宽高
const imgCanvasWidth = pageWidth;
const imgCanvasHeight = (592.28 / contentWidth) * contentHeight;
let imageHeight = imgCanvasHeight;
// 生成canvas截图,1表示生成的截图质量(0-1)
let pageData = canvas.toDataURL("image/jpeg", 1);
// new jsPdf接收三个参数,landscape表示横向,(默认不填是纵向),打印单位和纸张尺寸
let PDF = new jsPdf("", "pt", "a4");
// 当内容不超过a4纸一页的情况下
if (imageHeight < pageHeight) {
PDF.addImage(pageData, "JPEG", 20, 20, imgCanvasWidth, imgCanvasHeight);
} else {
// 当内容超过a4纸一页的情况下,需要增加一页
while (imageHeight > 0) {
PDF.addImage(
pageData,
"JPEG",
20,
position,
imgCanvasWidth,
imgCanvasHeight
);
imageHeight -= pageHeight;
position -= pageHeight;
// 避免添加空白页
if (imageHeight > 0) {
PDF.addPage();
}
}
}
return PDF;
}
function downPdf(pdfInstance: any, title: string) {
let maxFileNameLen = 50;
// 文件名过长导致下载失败
if (title.length > maxFileNameLen) {
title = title.substring(title.length - maxFileNameLen);
}
return pdfInstance.save(title + ".pdf", { returnPromise: true });
}
const handlePrintPDF = () => {
htmlToPdf("#wrap");
};
</script>
注意打印完之后最好在顶层组件封装个 reload
方法关联 v-if
通过 provide
提供下去,组件调用下重新渲染一遍(因为之前我们替换了页面元素,不刷新还原可能会有问题)
reload() {
this.isRouteAlive = false;
this.$nextTick(() => {
this.isRouteAlive = true;
});
},
最后这个截断问题比较棘手,我们可以这样解决
- 设置对应转换后的 canvas 为白色背景
- 转换图片后获取对应截断处的图片像素
- 从截断处一行行往上扫描像素点颜色
- 碰到这一行是全白的,代表从这里截断,将这个高度往下的内容放到下一页
具体代码如下!
<script setup lang="ts">
import HelloWorld from "./components/HelloWorld.vue";
import html2Canvas from "html2canvas";
import jsPdf from "jspdf";
import domtoimage from "dom-to-image";
async function htmlToPdf(selector) {
// 将html dom节点生成canvas
let htmlCanvas = await getCanvasByselector(selector);
// 将canvas对象转为pdf
let pdf = canvasToPdf(htmlCanvas, "我的pdf");
}
async function getCanvasByselector(selector) {
let printDom = document.querySelector(selector);
printDom.querySelectorAll("iframe").forEach((iframe) => {
// 创建文档碎片和新的div
let $fragment = document.createDocumentFragment();
let newDiv = document.createElement("p");
// ....中间可以设置newDiv属性和样式等....
// 拿到 iframe 里面每个元素复制
Array.from(iframe.contentWindow.document.body.children).forEach((ele) => {
$fragment.appendChild(ele);
});
newDiv.appendChild($fragment);
// 插入newDiv,内容和iframe一致
iframe.parentNode.insertBefore(newDiv, iframe);
// 移除iframe
iframe.remove();
});
// allowTaint: true 表示允许跨越的图片加载
// useCORS 是否尝试使用 CORS 从服务器加载图片
// scale 渲染的像素比率,默认为当前设备的设备像素比
let canvas = await html2Canvas(printDom, {
allowTaint: true,
useCORS: true,
scale: 1,
});
return canvas;
}
function canvasToPdf(canvas, title) {
//未生成pdf的html页面高度
let leftHeight = canvas.height;
let a4Width = 595.28;
let a4Height = 841.89; //A4大小,210mm x 297mm,四边各保留10mm的边距,显示区域190x277
//一页pdf显示html页面生成的canvas高度;
let a4HeightRef = Math.floor((canvas.width / a4Width) * a4Height);
//pdf页面偏移
let position = 0;
let pageData = canvas.toDataURL("image/jpeg", 1.0);
let pdf = new jsPdf("p", "pt", "a4"); //A4纸,纵向
let index = 1,
canvas1 = document.createElement("canvas"),
height;
pdf.setDisplayMode("fullwidth", "continuous", "FullScreen");
let pdfName = title;
function createImpl(canvas) {
console.log(leftHeight, a4HeightRef);
if (leftHeight > 0) {
index++;
let checkCount = 0;
if (leftHeight > a4HeightRef) {
let i;
for (i = position + a4HeightRef; i >= position; i--) {
let isWrite = true;
for (let j = 0; j < canvas.width; j++) {
let c = canvas.getContext("2d").getImageData(j, i, 1, 1).data;
if (c[0] != 0xff || c[1] != 0xff || c[2] != 0xff) {
isWrite = false;
break;
}
}
if (isWrite) {
checkCount++;
if (checkCount >= 10) {
break;
}
} else {
checkCount = 0;
}
}
height = Math.round(i - position) || Math.min(leftHeight, a4HeightRef);
console.log("Math.round(i - position)", Math.round(i - position));
console.log("Math.min(leftHeight, a4HeightRef);", Math.min(leftHeight, a4HeightRef));
console.log("height", height);
if (height <= 0) {
height = a4HeightRef;
}
} else { height = leftHeight;
}
canvas1.width = canvas.width;
canvas1.height = height;
console.log(index, "height:", height, "pos", position);
let ctx = canvas1.getContext("2d");
ctx.drawImage(
canvas,
0,
position,
canvas.width,
height,
0,
0,
canvas.width,
height
);
if (position != 0) {
pdf.addPage();
}
pdf.addImage(
canvas1.toDataURL("image/jpeg", 1.0),
"JPEG",
10,
10,
a4Width,
(a4Width / canvas1.width) * height
);
leftHeight -= height;
position += height;
if (leftHeight > 0) {
setTimeout(createImpl, 500, canvas);
} else {
pdf.save(pdfName + ".pdf");
}
}
}
//当内容未超过pdf一页显示的范围,无需分页
if (leftHeight < a4HeightRef) {
pdf.addImage(
pageData,
"JPEG",
0,
0,
a4Width,
(a4Width / canvas.width) * leftHeight
);
pdf.save(pdfName + ".pdf");
} else {
try {
pdf.deletePage(0);
setTimeout(createImpl, 500, canvas);
} catch (err) {
// console.log(err);
}
}
}
// function downPdf (pdfInstance: any, title: string) {
// let maxFileNameLen = 50;
// // 文件名过长导致下载失败
// if (title.length > maxFileNameLen) {
// title = title.substring(title.length - maxFileNameLen);
// }
// return pdfInstance.save(title + '.pdf', { returnPromise: true });
// }
const handlePrintPDF = () => {
htmlToPdf("#wrap");
};
</script>