Vue 生成 PDF 完整教程
目录
基础概念
在 Vue 中生成 PDF 主要分为两种方式:
方式一:HTML 转 PDF
- 流程:HTML DOM → 截图(Canvas)→ PDF
- 主要库 :
html2canvas+jspdf - 优势:支持复杂样式,接近浏览器显示效果
- 劣势:跨域问题,首次加载较慢
方式二:直接生成 PDF
- 流程:直接操作 PDF 对象
- 主要库 :
pdfkit,pdf-lib - 优势:性能高,文件小
- 劣势:需要手动排版,学习成本高
本教程重点 :使用 html2canvas + jspdf 实现 HTML 转 PDF,这是最常见的需求。
安装依赖
1. 安装核心库
bash
npm install html2canvas jspdf
2. 可选库(增强功能)
bash
# 图片压缩
npm install pica
# 多文件上传(与 API 通信)
npm install axios form-data
# 进度显示
npm install nprogress
3. 版本建议
json
{
"dependencies": {
"html2canvas": "^1.4.1",
"jspdf": "^2.5.1",
"axios": "^1.4.0",
"vue": "^2.7.0"
}
}
核心库详解
html2canvas
作用:将 DOM 元素转换为 Canvas 图片
基本语法:
javascript
import html2canvas from 'html2canvas'
const canvas = await html2canvas(element, {
// 选项配置
})
常用选项:
javascript
{
scale: 2, // 缩放因子(更清晰)
useCORS: true, // 跨域图片处理
logging: false, // 是否输出日志
allowTaint: true, // 允许污染的画布
backgroundColor: '#ffffff', // 背景颜色
width: 1000, // 宽度
height: 1000, // 高度
x: 0, // 偏移 x
y: 0, // 偏移 y
}
jsPDF
作用:创建 PDF 文档并添加内容
基本语法:
javascript
import jsPDF from 'jspdf'
const pdf = new jsPDF({
orientation: 'portrait', // 或 'landscape'
unit: 'mm', // 单位:mm, pt, px, in
format: 'a4', // 纸张大小
compress: true // 压缩内容
})
// 添加图片
pdf.addImage(imageData, 'PNG', x, y, width, height)
// 保存
pdf.save('filename.pdf')
简单实现
第一步:在 Vue 组件中引入库
vue
<template>
<div>
<div id="pdf-content">
<!-- 要导出的内容 -->
<h1>{{ title }}</h1>
<p>{{ content }}</p>
<table border="1">
<tr v-for="item in list" :key="item.id">
<td>{{ item.name }}</td>
<td>{{ item.value }}</td>
</tr>
</table>
</div>
<button @click="downloadPDF">下载 PDF</button>
</div>
</template>
<script>
import html2canvas from 'html2canvas'
import jsPDF from 'jspdf'
export default {
data() {
return {
title: '测试文档',
content: '这是测试内容',
list: [
{ id: 1, name: '项目 A', value: '100' },
{ id: 2, name: '项目 B', value: '200' }
]
}
},
methods: {
async downloadPDF() {
try {
// 第一步:获取 DOM 元素
const element = document.getElementById('pdf-content')
// 第二步:转换为 Canvas
const canvas = await html2canvas(element, {
scale: 2,
useCORS: true,
backgroundColor: '#ffffff'
})
// 第三步:Canvas 转 PDF
const imgData = canvas.toDataURL('image/png')
const pdf = new jsPDF({
orientation: 'portrait',
unit: 'mm',
format: 'a4'
})
// 计算缩放尺寸
const imgWidth = 210 // A4 宽度
const pageHeight = 297 // A4 高度
const imgHeight = (canvas.height * imgWidth) / canvas.width
pdf.addImage(imgData, 'PNG', 0, 0, imgWidth, imgHeight)
// 第四步:保存
pdf.save('report.pdf')
} catch (error) {
console.error('PDF 生成失败:', error)
this.$message.error('生成失败,请重试')
}
}
}
}
</script>
高级用法
多页 PDF
当内容超过一页时,需要分页处理:
javascript
async downloadPDF() {
const element = document.getElementById('pdf-content')
const canvas = await html2canvas(element)
const imgData = canvas.toDataURL('image/png')
const pdf = new jsPDF({
orientation: 'portrait',
unit: 'mm',
format: 'a4'
})
const imgWidth = 210
const pageHeight = 297
const imgHeight = (canvas.height * imgWidth) / canvas.width
let heightLeft = imgHeight
let position = 0
// 第一页
pdf.addImage(imgData, 'PNG', 0, position, imgWidth, imgHeight)
heightLeft -= pageHeight
// 后续页
while (heightLeft >= 0) {
position = heightLeft - imgHeight
pdf.addPage()
pdf.addImage(imgData, 'PNG', 0, position, imgWidth, imgHeight)
heightLeft -= pageHeight
}
pdf.save('report.pdf')
}
添加页码和页脚
javascript
async downloadPDF() {
const element = document.getElementById('pdf-content')
const canvas = await html2canvas(element)
const imgData = canvas.toDataURL('image/png')
const pdf = new jsPDF('portrait', 'mm', 'a4')
const pageCount = Math.ceil((canvas.height * 210) / canvas.width / 297)
let pageNum = 1
const addPageContent = () => {
if (pageNum > 1) {
pdf.addPage()
}
const position = (pageNum - 1) * (-297) + 20
pdf.addImage(imgData, 'PNG', 0, position, 210, (canvas.height * 210) / canvas.width)
// 添加页码
pdf.setFontSize(10)
pdf.text(
`第 ${pageNum} 页,共 ${pageCount} 页`,
pdf.internal.pageSize.getWidth() / 2,
pdf.internal.pageSize.getHeight() - 10,
{ align: 'center' }
)
pageNum++
}
for (let i = 0; i < pageCount; i++) {
addPageContent()
}
pdf.save('report.pdf')
}
隐藏不需要的元素
生成 PDF 时,有时需要隐藏某些按钮或控制元素:
javascript
async downloadPDF() {
// 隐藏按钮等不需要的元素
const buttons = document.querySelectorAll('.pdf-ignore')
const originalDisplay = []
buttons.forEach(btn => {
originalDisplay.push(btn.style.display)
btn.style.display = 'none'
})
try {
const element = document.getElementById('pdf-content')
const canvas = await html2canvas(element)
const imgData = canvas.toDataURL('image/png')
const pdf = new jsPDF()
pdf.addImage(imgData, 'PNG', 0, 0, 210, 297)
pdf.save('report.pdf')
} finally {
// 恢复显示
buttons.forEach((btn, index) => {
btn.style.display = originalDisplay[index]
})
}
}
优化性能:延迟渲染
当 DOM 包含大量数据时:
javascript
async downloadPDF() {
// 第一步:显示进度
this.$message.info('正在生成 PDF...')
// 第二步:等待 DOM 更新(重要!)
await this.$nextTick()
// 第三步:再延迟等待图片加载
await new Promise(resolve => setTimeout(resolve, 500))
// 第四步:生成 PDF
const element = document.getElementById('pdf-content')
const canvas = await html2canvas(element, {
scale: 2,
logging: false,
useCORS: true
})
const imgData = canvas.toDataURL('image/png')
const pdf = new jsPDF()
pdf.addImage(imgData, 'PNG', 0, 0, 210, 297)
pdf.save('report.pdf')
this.$message.success('PDF 已下载')
}
上传 PDF 到服务器
javascript
async downloadAndUploadPDF() {
try {
// 生成 PDF
const element = document.getElementById('pdf-content')
const canvas = await html2canvas(element)
const imgData = canvas.toDataURL('image/png')
const pdf = new jsPDF()
pdf.addImage(imgData, 'PNG', 0, 0, 210, 297)
// 转为 Blob
const pdfBlob = pdf.output('blob')
// 创建 FormData
const formData = new FormData()
formData.append('file', pdfBlob, 'report.pdf')
formData.append('userId', this.userId)
formData.append('timestamp', new Date().getTime())
// 上传到服务器
const response = await axios.post('/api/upload/pdf', formData, {
headers: {
'Content-Type': 'multipart/form-data'
}
})
if (response.data.code === 0) {
this.$message.success('上传成功')
} else {
this.$message.error(response.data.msg)
}
} catch (error) {
console.error('上传失败:', error)
this.$message.error('上传失败,请重试')
}
}
常见问题
1. 跨域图片问题
问题:图片无法加载到 PDF 中
解决方案:
javascript
// 方案 A:使用 useCORS 选项
const canvas = await html2canvas(element, {
useCORS: true,
allowTaint: true
})
// 方案 B:后端添加 CORS 头
// res.setHeader('Access-Control-Allow-Origin', '*')
// 方案 C:Base64 转换
async function imageToBase64(url) {
const response = await fetch(url)
const blob = await response.blob()
return new Promise(resolve => {
const reader = new FileReader()
reader.onloadend = () => resolve(reader.result)
reader.readAsDataURL(blob)
})
}
2. 文字模糊不清
问题:生成的 PDF 中文字不清晰
解决方案:
javascript
// 增加 scale 参数
const canvas = await html2canvas(element, {
scale: 2, // 或更高的值如 3 或 4
useCORS: true
})
3. 样式丢失
问题:某些 CSS 样式在 PDF 中不显示
解决方案:
css
/* 在 <style> 中使用内联样式,避免外部 CSS */
/* 使用 scoped 时可能出现问题,改为全局样式 */
/* ❌ 避免 */
<style scoped>
.content {
color: red;
}
</style>
/* ✅ 推荐 */
<style>
.pdf-content {
color: red;
font-size: 14px;
}
</style>
4. 分页时内容重复或缺失
问题:使用多页时某些内容重复或缺失
解决方案:
javascript
// 使用CSS分页属性
<style>
.section {
page-break-inside: avoid; /* 避免在元素内部分页 */
break-inside: avoid-page; /* 现代浏览器 */
page-break-after: avoid; /* 避免元素后分页 */
break-after: avoid-page; /* 现代浏览器 */
}
</style>
// 动态处理分页
async generatePDF() {
const elements = document.querySelectorAll('.section')
const pdf = new jsPDF()
for (let i = 0; i < elements.length; i++) {
if (i > 0) pdf.addPage()
const canvas = await html2canvas(elements[i])
const imgData = canvas.toDataURL('image/png')
pdf.addImage(imgData, 'PNG', 0, 0, 210, 297)
}
pdf.save('report.pdf')
}
5. 生成进度无法显示
问题:生成 PDF 时看不到进度反馈
解决方案:
javascript
async downloadPDF() {
this.isGenerating = true
this.progress = 0
try {
// 更新进度
this.progress = 10
const element = document.getElementById('pdf-content')
this.progress = 30
const canvas = await html2canvas(element, {
scale: 2,
useCORS: true
})
this.progress = 60
const imgData = canvas.toDataURL('image/png')
this.progress = 80
const pdf = new jsPDF()
pdf.addImage(imgData, 'PNG', 0, 0, 210, 297)
this.progress = 95
pdf.save('report.pdf')
this.progress = 100
this.$message.success('已完成')
} catch (error) {
console.error(error)
this.$message.error('生成失败')
} finally {
this.isGenerating = false
}
}
模板中显示进度:
vue
<el-progress :percentage="progress" v-if="isGenerating"></el-progress>
最佳实践
1. 模块化设计
创建一个 PDF 服务文件 src/services/pdfService.js:
javascript
import html2canvas from 'html2canvas'
import jsPDF from 'jspdf'
class PDFService {
// 基础 HTML 转 PDF
static async htmlToPDF(element, filename) {
try {
const canvas = await html2canvas(element, {
scale: 2,
useCORS: true,
backgroundColor: '#ffffff',
logging: false
})
const imgData = canvas.toDataURL('image/png')
const pdf = new jsPDF({
orientation: 'portrait',
unit: 'mm',
format: 'a4'
})
const imgWidth = 210
const pageHeight = 297
const imgHeight = (canvas.height * imgWidth) / canvas.width
let heightLeft = imgHeight
let position = 0
pdf.addImage(imgData, 'PNG', 0, position, imgWidth, imgHeight)
heightLeft -= pageHeight
while (heightLeft >= 0) {
position = heightLeft - imgHeight
pdf.addPage()
pdf.addImage(imgData, 'PNG', 0, position, imgWidth, imgHeight)
heightLeft -= pageHeight
}
pdf.save(`${filename}.pdf`)
return true
} catch (error) {
console.error('PDF 生成失败:', error)
return false
}
}
// 获取 PDF Blob
static async getPDFBlob(element) {
const canvas = await html2canvas(element)
const imgData = canvas.toDataURL('image/png')
const pdf = new jsPDF()
pdf.addImage(imgData, 'PNG', 0, 0, 210, 297)
return pdf.output('blob')
}
}
export default PDFService
在组件中使用:
javascript
import PDFService from '@/services/pdfService'
export default {
methods: {
async downloadPDF() {
await PDFService.htmlToPDF(
document.getElementById('pdf-content'),
'my-report'
)
}
}
}
2. 使用 Mixin 复用逻辑
创建 src/mixins/pdfMixin.js:
javascript
import html2canvas from 'html2canvas'
import jsPDF from 'jspdf'
export default {
data() {
return {
pdfProgress: {
visible: false,
percentage: 0,
message: ''
}
}
},
methods: {
async generatePDF(elementId, filename) {
this.pdfProgress.visible = true
this.pdfProgress.percentage = 0
try {
// 更新进度
const updateProgress = (percent, message) => {
this.pdfProgress.percentage = percent
this.pdfProgress.message = message
}
updateProgress(10, '正在准备...')
const element = document.getElementById(elementId)
updateProgress(30, '正在截图...')
const canvas = await html2canvas(element)
updateProgress(70, '正在生成 PDF...')
const imgData = canvas.toDataURL('image/png')
const pdf = new jsPDF()
pdf.addImage(imgData, 'PNG', 0, 0, 210, 297)
updateProgress(95, '正在保存...')
pdf.save(`${filename}.pdf`)
updateProgress(100, '完成')
this.$message.success('PDF 已下载')
} catch (error) {
console.error(error)
this.$message.error('生成失败')
} finally {
setTimeout(() => {
this.pdfProgress.visible = false
}, 1000)
}
}
}
}
在组件中使用:
vue
<script>
import pdfMixin from '@/mixins/pdfMixin'
export default {
mixins: [pdfMixin],
methods: {
downloadPDF() {
this.generatePDF('pdf-content', 'report')
}
}
}
</script>
3. 处理大数据量
javascript
async downloadLargePDF() {
// 方案:分块导出
const sections = document.querySelectorAll('.page-section')
const pdf = new jsPDF()
let isFirstPage = true
for (const section of sections) {
if (!isFirstPage) {
pdf.addPage()
}
const canvas = await html2canvas(section, {
scale: 2,
useCORS: true
})
const imgData = canvas.toDataURL('image/png')
pdf.addImage(imgData, 'PNG', 0, 0, 210, 297)
isFirstPage = false
}
pdf.save('large-report.pdf')
}
4. 错误处理和重试
javascript
async downloadPDFWithRetry(maxRetries = 3) {
let retries = 0
while (retries < maxRetries) {
try {
const element = document.getElementById('pdf-content')
const canvas = await html2canvas(element)
const imgData = canvas.toDataURL('image/png')
const pdf = new jsPDF()
pdf.addImage(imgData, 'PNG', 0, 0, 210, 297)
pdf.save('report.pdf')
this.$message.success('生成成功')
return true
} catch (error) {
retries++
console.warn(`第 ${retries} 次尝试失败:`, error)
if (retries >= maxRetries) {
this.$message.error('生成失败,请稍后重试')
return false
}
// 延迟后重试
await new Promise(resolve => setTimeout(resolve, 1000 * retries))
}
}
}
总结
| 功能 | 方案 | 难度 |
|---|---|---|
| 基础 HTML 转 PDF | html2canvas + jsPDF | ⭐ |
| 多页 PDF | 手动分页处理 | ⭐⭐ |
| 自定义排版 | PDF 坐标定位 | ⭐⭐⭐ |
| 上传到服务器 | FormData + axios | ⭐⭐ |
| 性能优化 | 分块、延迟、缓存 | ⭐⭐⭐ |