Vue 生成 PDF 完整教程

Vue 生成 PDF 完整教程

目录

  1. 基础概念
  2. 安装依赖
  3. 核心库详解
  4. 简单实现
  5. 高级用法
  6. 常见问题
  7. 最佳实践

基础概念

在 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 ⭐⭐
性能优化 分块、延迟、缓存 ⭐⭐⭐
相关推荐
就叫飞六吧1 天前
css+js 前端无限画布实现
前端·javascript·css
2501_941148151 天前
高并发搜索引擎Elasticsearch与Solr深度优化在互联网实践分享
java·开发语言·前端
IT 前端 张1 天前
Uniapp全局显示 悬浮组件/无需单页面引入
前端·javascript·uni-app
allenjiao1 天前
WebGPU vs WebGL:WebGPU什么时候能完全替代WebGL?Web 图形渲染的迭代与未来
前端·图形渲染·webgl·threejs·cesium·webgpu·babylonjs
上车函予1 天前
geojson-3d-renderer:从原理到实践,打造高性能3D地理可视化库
前端·vue.js·three.js
孟祥_成都1 天前
别被营销号误导了!你以为真的 Bun 和 Deno 比 Node.js 快很多吗?
前端·node.js
Lsx_1 天前
🔥Vite+ElementPlus 自动按需加载与主题定制原理全解析
前端·javascript·element
零一科技1 天前
Vue3拓展:实现原理 - 浅析
前端·vue.js
抱琴_1 天前
【Vue3】从混乱到有序:我用 1 个 Vue Hooks 搞定大屏项目所有定时任务
前端·vue.js
文心快码BaiduComate1 天前
用文心快码写个「隐私优先」的本地会议助手
前端·后端·程序员