在最近的一个项目中,遇到一个将页面内容(详情页)导出为 PDF的需求,但是好像目前没有直接把dom转成pdf这样一步到位的技术,所以自己封装了一个间接转换的方法,基于 Vue3 + TypeScript 的通用 Hook 封装,利用 html2canvas 和 jspdf 实现网页内容导出为 PDF,并解决了 滚动截断 、 清晰度不足 以及 自动分页 等常见问题。
在实现过程中,我们通常会遇到以下几个坑:
新建文件 useExportPdf.ts,下载依赖 html2canvas 和 jspdf 然后引入:
import html2canvas from 'html2canvas';
import jsPDF from 'jspdf';
/**
* 导出页面为 PDF
* @param dom 需要导出的 DOM 元素
* @param fileName 导出的文件名(不含后缀)
*/
export const useExportPDF = async (dom: HTMLElement, fileName: string) => {
const element = dom;
if (!element) {
console.error('导出失败,未找到导出元素');
return;
}
// 1. 解决滚动截断问题:获取元素实际高度
const originalHeight = element.scrollHeight;
// 临时设置高度为 auto,确保能截取到所有内容
const originalStyleHeight = element.style.height;
element.style.height = 'auto';
try {
// 2. 将 DOM 转换为 Canvas
const canvas = await html2canvas(element, {
useCORS: true, // 允许跨域图片
scale: 2, // 2倍缩放,解决模糊问题
scrollY: -window.scrollY, // 修正滚动条偏移
scrollX: 0,
windowHeight: originalHeight, // 告诉 html2canvas 完整高度
});
// 3. 初始化 PDF 实例
// p: 纵向, mm: 单位毫米, a4: 纸张格式
const pdf = new jsPDF('p', 'mm', 'a4');
// A4 纸内容宽度(留边距)
const imgWidth = 190;
// 根据宽度计算等比例的高度
const imgHeight = (canvas.height * imgWidth) / canvas.width;
// 获取 PDF 页面可用高度
const pdfPageHeight = pdf.internal.pageSize.getHeight();
// 4. 处理分页逻辑
let position = 0;
// 第一页
pdf.addImage(canvas, 'PNG', 10, 10 - position, imgWidth, imgHeight);
position += pdfPageHeight; // 这里简化处理,按页面高度分页
// 如果内容高度超过一页,循环添加新页
while (position < imgHeight) {
pdf.addPage();
// 移动图片位置,实现视觉上的“接续”
// 注意:这里简单的 position += pageHeight 可能需要根据实际情况调整,
// 比如减去一些边距来防止文字被切断,Demo 中使用了简化的逻辑。
pdf.addImage(canvas, 'PNG', 10, 10 - position, imgWidth, imgHeight);
position += pdfPageHeight;
}
// 5. 保存文件
pdf.save(`${fileName}.pdf`);
} catch (error) {
console.error('导出 PDF 异常:', error);
} finally {
// 6. 恢复原始样式
element.style.height = originalStyleHeight;
}
};
在 Vue 组件中,我们只需要获取到 DOM 引用,然后调用这个 Hook 即可。
导出PDF
import { useExportPDF } from '/@/hooks/exportpdf/useExportpdf';
// 导出的 DOM 元素
const pdfContainer = ref(null);
// 导出
const exportPDF = async () => {
loading.value = true;
try {
await useExportPDF(pdfContainer.value, 'xxxxpdf');
loading.value = false;
} catch (e) {
console.log(e);
loading.value = false;
}
};
通过这个封装,我们实现了一个轻量级且功能完备的 PDF 导出工具。它不仅解决了最让人头疼的 长页面截断 问题,还通过 scale 参数保证了导出的清晰度。
为啥img标签就能通过图片url加载图片,但是把图片转成Canvas就会出现跨域问题?
简单来说就是 标签只是“展示”数据,而 转成Canvas 需要“读取”数据 。浏览器的安全策略(同源策略)就是“看一眼”和“拿走数据”的区别
标签展示数据跟把图片转成转成Canvas浏览器都会请求图片,服务器返回图片数据。区别在于:
1. 标签加载
请求头 :浏览器发起请求时, Origin 字段可能不被包含(或者是 null ),或者仅仅作为 Referer 发送。它通常被视为一个“简单请求”。
响应头 :服务器返回图片数据。通常 不需要 包含 Access-Control-Allow-Origin 等 CORS 相关头信息。
结果 :浏览器接收到数据,渲染引擎直接解码并在屏幕上绘制像素。JavaScript 无法接触到这些数据。
2.开启 CORS 的情况( crossorigin="anonymous" 或 Canvas 请求)
当你为了 Canvas 导出而给图片添加 crossorigin 属性,或者使用 JS fetch 请求图片时:
请求头 :浏览器 强制添加 Origin: https://你的域名.com 字段,明确告诉服务器是谁在请求。
响应头(关键差别) :
结果 :
。
