前端导出 PDF 方案
用到的包
html2canvas 将 DOM 元素转换为 canvas 图像
jspdf 将 canvas 图像转换为 PDF 文件
print-js 打印 PDF 文件
前端导出方案
前端导出的工作原理
直接在浏览器端捕获 DOM 元素
使用 canvas 技术将 DOM 转换为图像
将图像数据转换为 PDF 文件
触发浏览器下载功能
前端导出的优势:
实现简单,无需后端额外开发
可以完全保留前端的样式和布局
即时生成,无需等待服务器响应
减轻服务器压力
前端导出的劣势
大量 DOM 元素处理可能导致浏览器性能问题
不同浏览器兼容性可能存在差异
跨域资源处理需要额外配置
前端封装
- 封装一个工具函数
js
/**
* @Author: jsopy
* @Date: 2025-12-07 16:51:36
* @LastEditTime: 2025-12-07 19:01:08
* @FilePath: /testpdf/src/utils/html2pdf.js
* @Description:
* @ 参数1.fileName - 可选,自定义文件名,如果未提供则使用默认格式
* @ 参数2.pdfId - 必填,需要导出的pdf的id
*/
import html2Canvas from "html2canvas";
import { jsPDF } from "jspdf";
import printJS from "print-js";
export const exportpdftools = {
// 导出为PDF文件
// 参数说明:fileName - 可选,自定义文件名,如果未提供则使用默认格式
// 返回Promise,便于外部组件获取导出状态
exportToPdf(fileName, pdfId) {
return new Promise((resolve, reject) => {
// 如果未提供文件名,则使用默认格式
const pdfFileName =
fileName || "简历_" + this.getNowFormatDate() + ".pdf";
var element = document.getElementById(pdfId); // 这个dom元素是要导出pdf的div容器
var w = element.offsetWidth; // 获得该容器的宽
var h = element.offsetHeight; // 获得该容器的高
var offsetTop = element.offsetTop + 30; // 获得该容器到文档顶部的距离
var offsetLeft = element.offsetLeft; // 获得该容器到文档最左的距离
var canvas = document.createElement("canvas");
var abs = 0;
var win_i = document.body.clientWidth; // 获得当前可视窗口的宽度(不包含滚动条)
var win_o = window.innerWidth; // 获得当前窗口的宽度(包含滚动条)
if (win_o > win_i) {
abs = (win_o - win_i) / 2; // 获得滚动条长度的一半
}
canvas.width = w * 2; // 将画布宽&&高放大两倍
canvas.height = h * 2;
var context = canvas.getContext("2d");
context.scale(2, 2);
context.translate(-offsetLeft - abs, -offsetTop);
// 注意:不需要在主文档中预先查找元素,因为html2canvas会在克隆的iframe中工作
// 所有元素判断都应该在ignoreElements函数内部基于当前元素的属性进行
html2Canvas(element, {
allowTaint: true, //允许跨域
useCORS: true, //允许图片跨域
scale: 2, // 提升画面质量,但是会增加文件大小
ignoreElements: function (el) {
try {
// 安全地检查元素类名
const hasClass = (element, className) => {
if (!element || !element.classList) returnfalse;
try {
return element.classList.contains(className);
} catch (e) {
returnfalse;
}
};
// 安全地检查元素类型
const getElementTagName = (element) => {
if (!element || !element.tagName) return "";
try {
return element.tagName.toLowerCase();
} catch (e) {
return "";
}
};
const tagName = getElementTagName(el);
// 安全地获取元素的实际文本内容(仅用于按钮检测)
const getElementText = (element) => {
if (!element) return "";
try {
// 仅去除空格和换行符,保留原始大小写
return (element.textContent || element.innerText || "").replace(
/\s+/g,
""
);
} catch (e) {
return "";
}
};
// 检查是否是按钮元素
const isButton =
tagName === "a-button" ||
hasClass(el, "ant-btn") ||
tagName === "button";
// 检查按钮文本内容
const elementText = getElementText(el);
const isDownloadButton =
isButton &&
(elementText.includes("下载简历") ||
elementText.includes("下载"));
const isActionButton =
isButton &&
(elementText.includes("编辑") ||
elementText.includes("删除") ||
elementText.includes("新增"));
// 排除特定元素:分页控件、审批人员区域和操作按钮容器
const isPagination = hasClass(el, "ant-pagination");
const isApprovePeople = el.id === "approvePeople";
const isJBtnsContainer = hasClass(el, "jBtns");
// 综合所有排除条件 - 只排除真正的操作按钮和特定容器,避免误删标题
const shouldIgnore =
isDownloadButton ||
isActionButton ||
isJBtnsContainer ||
isPagination ||
isApprovePeople;
return shouldIgnore;
} catch (e) {
// 如果发生任何错误,默认不忽略该元素
console.error("Error in ignoreElements:", e);
returnfalse;
}
},
})
.then(async (canvas) => {
var contentWidth = canvas.width;
var contentHeight = canvas.height;
//a4纸的尺寸[595.28,841.89],html页面生成的canvas在pdf中图片的宽高
var imgWidth = 595.28;
var imgHeight = (592.28 / contentWidth) * contentHeight;
var pageHeight = 841.89; // A4纸的高度
var position = 50; // 页面偏移
// 第一个参数方向 第二个参数单位 第三个参数尺寸
var pdf = new jsPDF("", "pt", "a4");
// 调整边距:增加左侧padding(20px),减少右侧空白
const leftPadding = 20; // 左侧边距
const adjustedImgWidth = imgWidth - leftPadding; // 调整后的图片宽度
// 解决黑色块问题的优化分页实现
if (contentHeight < pageHeight) {
// 当内容未超过pdf一页显示的范围,无需分页
// 先设置白色背景
pdf.setFillColor(255, 255, 255);
pdf.rect(0, 0, imgWidth, pageHeight, "F");
// 再添加图片内容
var pageData = canvas.toDataURL("image/jpeg", 1.0);
pdf.addImage(
pageData,
"JPEG",
leftPadding,
position,
adjustedImgWidth,
imgHeight
);
} else {
// 计算页面数量 - 使用更合理的计算方式
var pageHeightInCanvas = (pageHeight * contentWidth) / 592.28; // A4高度在canvas中的像素值
var pageCount = Math.ceil(contentHeight / pageHeightInCanvas);
// 逐页添加内容
for (var i = 0; i < pageCount; i++) {
// 添加新页面(除了第一页)
if (i > 0) pdf.addPage();
// 为每一页创建新的canvas
var newCanvas = document.createElement("canvas");
newCanvas.width = contentWidth;
newCanvas.height = Math.min(
pageHeightInCanvas,
contentHeight - i * pageHeightInCanvas
);
var ctx = newCanvas.getContext("2d");
// 设置白色背景,避免黑色块出现
ctx.fillStyle = "#ffffff";
ctx.fillRect(0, 0, newCanvas.width, newCanvas.height);
// 绘制当前页的内容
ctx.drawImage(
canvas,
0,
i * pageHeightInCanvas,
contentWidth,
newCanvas.height
);
// 将新canvas转换为图片数据
var newPageData = newCanvas.toDataURL("image/jpeg", 1.0);
// 计算当前页的图片高度
var currentPageHeight =
(592.28 / contentWidth) * newCanvas.height;
// 先设置白色背景
pdf.setFillColor(255, 255, 255);
pdf.rect(0, 0, imgWidth, pageHeight, "F");
// 再添加图片内容
pdf.addImage(
newPageData,
"JPEG",
leftPadding,
position,
adjustedImgWidth,
currentPageHeight
);
}
}
// 直接下载PDF文件而不是打开打印预览
pdf.save(pdfFileName);
// 保留原有的打印功能,但需要明确调用
this.showPrintSuccessMessage = true;
setTimeout(() => {
this.showPrintSuccessMessage = false;
}, 3000);
// 通知导出成功完成
resolve();
})
.catch((error) => {
console.error("PDF导出失败:", error);
reject(error);
});
});
},
// 打印功能
printResume() {
var element = document.getElementById(pdfId);
html2Canvas(element, {
allowTaint: true,
useCORS: true,
scale: 2,
}).then((canvas) => {
const jsPDFBytes = canvas.toDataURL("image/jpeg", 1.0);
printJS({ printable: jsPDFBytes, type: "image", showModal: true });
});
},
// 获取当前格式化日期
getNowFormatDate() {
var date = newDate();
var seperator1 = "";
var year = date.getFullYear();
var month = date.getMonth() + 1;
var strDate = date.getDate();
var hour = date.getHours();
var min = date.getMinutes();
var second = date.getSeconds();
if (month >= 1 && month <= 9) {
month = "0" + month;
}
if (strDate >= 0 && strDate <= 9) {
strDate = "0" + strDate;
}
if (hour >= 0 && hour <= 9) {
hour = "0" + hour;
}
if (min >= 0 && min <= 9) {
min = "0" + min;
}
if (second >= 0 && second <= 9) {
second = "0" + second;
}
var currentdate =
year +
seperator1 +
month +
seperator1 +
strDate +
seperator1 +
hour +
seperator1 +
min +
seperator1 +
second;
return currentdate;
},
};前端使用
vue
<template>
<div>
<div class="title">前端导出PDF 需要使用两个类库 1.html2canvas 2.jspdf</div>
<el-button @click="handlepdf">导出pdf </el-button>
<div id="DomPdf" style="width: 800px; height: 1200px"></div>
</div>
</template>
<script setup>
import { exportpdftools } from "@/utils/html2pdf";
const isExporting = ref(false);
const handlepdf = async () => {
// 导出前设置导出状态为true
isExporting.value = true;
// 导出PDF,第一个参数就是文件名,第二个参数就是要导出的DOM元素
exportpdftools
.exportToPdf("测试PDF文件", "app")
.then((res) => {
console.log(res);
// 导出后设置导出状态为false
isExporting.value = false;
})
.catch((e) => {
console.log(e);
// 导出后设置导出状态为false
isExporting.value = false;
});
};
</script>
<style lang="scss" scoped></style>