封装好用的页面导出 PDF 工具 Hook (html2canvas + jspdf)
在最近的一个项目中,遇到将页面内容(详情页)导出为 PDF 的需求。目前似乎没有直接将 DOM 转为 PDF 的一步到位技术,因此封装了一个间接转换的方法。基于 Vue3 + TypeScript 的通用 Hook 封装,利用 html2canvas 和 jspdf 实现网页内容导出为 PDF,并解决了滚动截断、清晰度不足以及自动分页等常见问题。
一、技术选型
- html2canvas: 将 DOM 元素转换为 Canvas 图片。
- jspdf: 将 Canvas 图片生成 PDF 文件。
- 封装: 使用 Hook 方式封装,方便复用。
二、核心痛点与解决方案
在实现过程中,通常会遇到以下几个问题:
- 导出内容不全: 如果页面有滚动条,直接截图只能截取可视区域。
- 解法: 在截图前将 DOM 高度设置为 auto,并获取 scrollHeight 传递给 html2canvas 的 windowHeight 参数。
- 图片模糊: 默认截图出来的 PDF 很模糊。
- 解法: 设置 scale: 2,提高 Canvas 的像素密度。
- PDF 分页问题: 长图直接放入 PDF 会被压缩变形。
- 解法: 计算内容高度与 A4 纸高度的比例,通过循环 addPage() 实现自动分页切割。
三、源码实现
新建文件 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 即可。
<template>
<a-button type="primary" @click="exportPDF" v-if="disabled">
导出 PDF
</a-button>
</template>
<script setup lang="ts">
import { ref } from 'vue';
import { useExportPDF } from '/@/hooks/exportpdf/useExportpdf';
// 导出的 DOM 元素
const pdfContainer = ref<HTMLDivElement>(null);
const loading = ref(false);
// 导出
const exportPDF = async () => {
loading.value = true;
try {
await useExportPDF(pdfContainer.value, 'xxxxpdf');
} catch (e) {
console.log(e);
} finally {
loading.value = false;
}
};
</script>
五、值得注意的事项
- 跨域问题: html2canvas 将 DOM 元素转成 Canvas 图片时,如果 DOM 中有图片,需要解决跨域问题。一般可以在服务端(图片源)设置图片的响应头(Response Header),必须包含 CORS 头,允许你的域名访问;或者 Nginx 配置下代理;或者前端解决的话就把图片转成 Base64 格式。但如果图片比较多,不建议前端解决,因为转图片格式消耗时间多,且 Base64 会增加文件大小。
- 忽略元素: 如果想导致的内容之中,有些是不用导出,或者根据不同条件来区分是否导出,可以使用
data-html2canvas-ignore属性,设置为 true 就不会导出这个元素。
六、总结
通过这个封装,我们实现了一个轻量级且功能完备的 PDF 导出工具。它不仅解决了最让人头疼的长页面截断问题,还通过 scale 参数保证了导出的清晰度。
七、小思考
为什么 img 标签就能通过图片 URL 加载图片,但是把图片转成 Canvas 就会出现跨域问题?
简单来说就是 img 标签只是'展示'数据,而转成 Canvas 需要'读取'数据。浏览器的安全策略(同源策略)就是'看一眼'和'拿走数据'的区别。
img 标签展示数据跟把图片转成 Canvas 浏览器都会请求图片,服务器返回图片数据。区别在于:
-
img 标签加载
- 请求头: 浏览器发起请求时,Origin 字段可能不被包含(或者是 null),或者仅仅作为 Referer 发送。它通常被视为一个'简单请求'。
- 响应头: 服务器返回图片数据。通常不需要包含 Access-Control-Allow-Origin 等 CORS 相关头信息。
- 结果: 浏览器接收到数据,渲染引擎直接解码并在屏幕上绘制像素。JavaScript 无法接触到这些数据。
-
开启 CORS 的情况(crossorigin="anonymous" 或 Canvas 请求)
- 请求头: 浏览器强制添加 Origin: [your-domain.com] 字段,明确告诉服务器是谁在请求。
- 响应头(关键差别):
- 如果服务器支持跨域:必须返回 Access-Control-Allow-Origin: * 或 Access-Control-Allow-Origin: [your-domain.com]。
- 如果服务器不支持:服务器可能正常返回了图片数据(状态码 200),但缺少了 CORS 响应头。
- 结果:
- 如果有 CORS 头:浏览器认为这份数据是'安全'的,允许 Canvas 读取和导出。
- 如果没有 CORS 头:虽然数据下载下来了,但浏览器(网络层或渲染层)会拦截这次加载,报错 CORS policy,图片甚至可能直接裂开(加载失败),更别说画到 Canvas 上了。


