基于 html2canvas 与 jsPDF 实现 Vue 页面长截图导出 PDF
在 Vue 前端项目中,若需将当前页面内容(包括动态渲染的数据)转换为 PDF 文件,一种常见的技术方案是利用 html2canvas 对 DOM 节点进行位图捕获,再结合 jsPDF 将生成的图像数据封装为 PDF 文档。该方案支持长页面自动分页处理,并允许添加水印等自定义样式。
1. 依赖安装
首先,通过 npm 安装必要的库:
npm install html2canvas jspdf --save
2. 核心工具类封装
创建一个独立的 JavaScript 模块(例如 pdfExporter.js),用于封装导出逻辑。此模块将定义一个全局方法或插件,以便在 Vue 组件中调用。
关键配置说明:
- A4 尺寸适配:A4 纸物理尺寸为 210mm x 297mm。代码中通常预留边距,例如设置有效显示区域宽度为 170mm,高度为 257mm。
- DOM 选择器:必须确保目标元素具有唯一的 ID 或 Class,且该元素包含所有需要导出的内容。
- 跨域图片处理:若页面包含外部图片,需启用
useCORS: true并确保服务器支持 CORS,否则可能导致截图失败或图片丢失。 - 清晰度优化:通过调整
dpi参数可提升生成图像的分辨率。
以下是封装后的代码示例,包含自动分页和水印添加功能:
import html2canvas from 'html2canvas'
import { jsPDF } from 'jspdf'
// A4 纸张尺寸常量 (单位: mm)
const PAGE_WIDTH = 170; // 减去左右边距后的可用宽度
const PAGE_HEIGHT = 257; // 减去上下边距后的可用高度
/**
* 向 PDF 每一页添加水印
* @param {Object} doc - jsPDF 实例
* @param {String} watermarkSrc - 水印图片路径或 Base64
*/
function applyWatermark(doc, watermarkSrc) {
const totalPages = doc.internal.getNumberOfPages();
for (let i = 1; i <= totalPages; i++) {
doc.setPage(i);
try {
// 假设水印图片已预加载或为本地资源
doc.addImage(watermarkSrc, 'PNG', 0, 0, 210, 297);
} catch (e) {
console.warn('Failed to add watermark:', e);
}
}
return doc;
}
/**
* 将指定 DOM 元素导出为 PDF
* @param {String} elementId - 目标元素的 ID
* @param {String} fileName - 下载文件名
* @param {String} watermarkPath - 可选的水印图片路径
*/
export async function exportDomToPdf(elementId, fileName, watermarkPath = null) {
const element = document.getElementById(elementId);
if (!element) {
throw new Error(`Element with ID "${elementId}" not found.`);
}
// 1. 使用 html2canvas 截取 DOM
const canvas = await html2canvas(element, {
scale: 2, // 提高清晰度
useCORS: true,
logging: false,
backgroundColor: '#ffffff' // 确保背景不透明
});
// 2. 初始化 jsPDF 实例
const pdf = new jsPDF('p', 'mm', 'a4');
// 计算单页图像在 Canvas 中的像素高度
// 比例关系: canvasWidth / PAGE_WIDTH = imgHeightInPixels / PAGE_HEIGHT
const imgWidthPx = canvas.width;
const imgHeightPx = Math.floor((PAGE_HEIGHT * imgWidthPx) / PAGE_WIDTH);
let heightLeft = canvas.height;
let position = 0;
let pageIndex = 0;
// 3. 循环切片并添加到 PDF
while (heightLeft > 0) {
// 创建临时 Canvas 用于裁剪当前页的内容
const pageCanvas = document.createElement('canvas');
pageCanvas.width = imgWidthPx;
// 确定当前页实际绘制的高度(防止最后一张图超出边界)
const drawHeight = Math.min(imgHeightPx, heightLeft);
pageCanvas.height = drawHeight;
const ctx = pageCanvas.getContext('2d');
// 从原 Canvas 中提取对应区域
ctx.drawImage(
canvas,
0, position,
imgWidthPx, drawHeight,
0, 0,
imgWidthPx, drawHeight
);
// 将临时 Canvas 转为 Data URL
const imgData = pageCanvas.toDataURL('image/jpeg', 0.95);
// 添加到 PDF
if (pageIndex === 0) {
pdf.addImage(imgData, 'JPEG', 10, 10, PAGE_WIDTH, drawHeight * (PAGE_WIDTH / imgWidthPx));
} else {
pdf.addPage();
pdf.addImage(imgData, 'JPEG', 10, 10, PAGE_WIDTH, drawHeight * (PAGE_WIDTH / imgWidthPx));
}
heightLeft -= imgHeightPx;
position += imgHeightPx;
pageIndex++;
}
// 4. 可选:添加水印
if (watermarkPath) {
applyWatermark(pdf, watermarkPath);
}
// 5. 保存文件
pdf.save(`${fileName}.pdf`);
}
3. Vue 项目集成
有两种主要方式在 Vue 中使用上述工具函数:
方式一:全局挂载(推荐用于频繁使用的场景)
在 main.js 中引入并注册为 Vue 原型方法,使得所有组件均可直接调用。
import Vue from 'vue';
import { exportDomToPdf } from '@/utils/pdfExporter'; // 根据实际路径调整
// 挂载到 Vue 原型链
Vue.prototype.$exportPdf = exportDomToPdf;
new Vue({
render: h => h(App),
}).$mount('#app')
方式二:局部引入
仅在需要的组件中引入函数,避免污染全局命名空间。
<script>
import { exportDomToPdf } from '@/utils/pdfExporter';
export default {
methods: {
handleExport() {
// 假设模板中有一个 id 为 "report-content" 的 div
exportDomToPdf('report-content', 'MyReport');
}
}
}
</script>
<template>
<div>
<button @click="handleExport">导出 PDF</button>
<div id="report-content">
<!-- 需要导出的复杂内容,如表格、图表等 -->
<p>Hello World</p>
</div>
</div>
</template>
4. 注意事项与常见问题
- 字体问题:
html2canvas对某些特殊字体或 Web Font 的支持可能不佳,建议在 CSS 中使用系统默认字体或确保字体文件正确加载。 - SVG 兼容性:部分旧版本的
html2canvas对 SVG 支持有限,如果遇到图标缺失,尝试将 SVG 转换为 PNG 或使用内联 Base64。 - 内存占用:对于极长的页面,生成的 Canvas 图像可能非常大,导致浏览器内存溢出。建议分段导出或优化页面结构。
- 异步渲染:如果页面内容依赖于异步请求(如 API 数据),务必在数据完全渲染后再调用导出方法,可使用
this.$nextTick()或等待 Promise 完成。