在现代Web应用开发中,将数据导出为PDF是一项常见且重要的功能需求。PDF作为一种通用的文档格式,具有跨平台、保持格式一致、易于打印等优势。Vue.js作为当前流行的前端框架,提供了多种实现PDF导出的方法。本文将全面探讨Vue环境下实现PDF导出的7种主要方法,包括原生浏览器API、常用第三方库方案以及服务器端导出方案,每种方法都将提供详细的代码示例和优劣分析。
这种方法利用浏览器自带的打印功能,通过CSS媒体查询控制打印样式。
实现原理:
代码示例:
销售报表
日期
产品
数量
金额
{{ item.date }}
{{ item.product }}
{{ item.quantity }}
{{ formatCurrency(item.amount) }}
优点:
缺点:
对于更专业的打印需求,可以使用CSS Paged Media模块定义分页、页眉页脚等。
代码示例:
@page {
size: A4;
margin: 1cm;
@top-left {
content: "公司名称";
font-size: 10pt;
}
@top-right {
content: "页码 " counter(page) " / " counter(pages);
font-size: 10pt;
}
@bottom-center {
content: "机密文件";
font-size: 8pt;
color: #999;
}
}
@media print {
h1 {
page-break-before: always;
}
table {
page-break-inside: avoid;
}
.page-break {
page-break-after: always;
}
}
优点:
缺点:
jsPDF是最流行的JavaScript PDF生成库之一,可以直接在浏览器中生成PDF。
安装:
npm install jspdf
基础实现:
import jsPDF from 'jspdf'
export function generatePDF(title, data, columns, filename = 'export.pdf') {
const doc = new jsPDF()
// 添加标题
doc.setFontSize(18)
doc.text(title, 14, 15)
// 添加表头
doc.setFontSize(12)
let y = 30
columns.forEach((col, index) => {
doc.text(col.label, 14 + index * 40, y)
})
// 添加数据行
doc.setFontSize(10)
data.forEach((row, rowIndex) => {
y = 40 + rowIndex * 10
if (y > 280) { // 接近页面底部
doc.addPage()
y = 20
}
columns.forEach((col, colIndex) => {
doc.text(String(row[col.field]), 14 + colIndex * 40, y)
})
})
// 保存文件
doc.save(filename)
}
// Vue组件中使用
methods: {
exportPDF() {
const data = [
{ id: 1, name: '产品A', price: 100, stock: 50 },
{ id: 2, name: '产品B', price: 200, stock: 30 }
]
const columns = [
{ label: 'ID', field: 'id' },
{ label: '产品名称', field: 'name' },
{ label: '价格', field: 'price' },
{ label: '库存', field: 'stock' }
]
generatePDF('产品列表', data, columns)
}
}
高级功能示例:
function generateAdvancedPDF() {
const doc = new jsPDF({
orientation: 'landscape',
unit: 'mm',
format: 'a4'
})
// 设置元数据
doc.setProperties({
title: '高级报表',
subject: '2023年度销售数据',
author: '销售系统',
keywords: '销售, 报表, 2023',
creator: '公司销售系统'
})
// 添加公司logo
const imgData = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUg...'
doc.addImage(imgData, 'PNG', 10, 10, 50, 15)
// 添加标题
doc.setFont('helvetica', 'bold')
doc.setFontSize(22)
doc.setTextColor(40, 40, 40)
doc.text('2023年度销售报表', 105, 20, { align: 'center' })
// 添加表格
const headers = [['产品', 'Q1', 'Q2', 'Q3', 'Q4', '总计']]
const salesData = [
['产品A', 1000, 1500, 1200, 1800, 5500],
['产品B', 800, 900, 1000, 1200, 3900],
['产品C', 500, 600, 700, 900, 2700]
]
doc.autoTable({
head: headers,
body: salesData,
startY: 40,
theme: 'grid',
headStyles: {
fillColor: [22, 160, 133],
textColor: 255,
fontStyle: 'bold'
},
alternateRowStyles: {
fillColor: [240, 240, 240]
},
margin: { top: 40 },
didDrawPage: function(data) {
// 页脚
doc.setFontSize(10)
doc.setTextColor(150)
const pageCount = doc.internal.getNumberOfPages()
doc.text(`第 ${doc.internal.getCurrentPageInfo().pageNumber} 页 / 共 ${pageCount} 页`,
data.settings.margin.left,
doc.internal.pageSize.height - 10
)
}
})
// 添加折线图(使用jsPDF的简单绘图功能)
doc.setDrawColor(100, 100, 255)
doc.setFillColor(100, 100, 255)
const points = [
{ x: 60, y: 150, q: 'Q1' },
{ x: 90, y: 130, q: 'Q2' },
{ x: 120, y: 140, q: 'Q3' },
{ x: 150, y: 120, q: 'Q4' }
]
// 绘制折线
points.forEach((point, i) => {
if (i > 0) {
doc.line(points[i-1].x, points[i-1].y, point.x, point.y)
}
doc.circle(point.x, point.y, 2, 'F')
doc.text(point.q, point.x, point.y + 10)
})
doc.save('advanced_report.pdf')
}
优点:
缺点:
这种方案先将HTML转换为canvas,再将canvas转为PDF,适合需要精确复制页面样式的场景。
安装:
npm install html2canvas jspdf
基础实现:
import html2canvas from 'html2canvas'
import jsPDF from 'jspdf'
export async function exportHTMLToPDF(element, filename = 'export.pdf', options = {}) {
const canvas = await html2canvas(element, {
scale: 2, // 提高分辨率
logging: false,
useCORS: true,
allowTaint: true,
...options
})
const imgData = canvas.toDataURL('image/png')
const pdf = new jsPDF({
orientation: canvas.width > canvas.height ? 'landscape' : 'portrait',
unit: 'mm'
})
const pageWidth = pdf.internal.pageSize.getWidth()
const pageHeight = pdf.internal.pageSize.getHeight()
const imgWidth = pageWidth
const imgHeight = (canvas.height * imgWidth) / canvas.width
pdf.addImage(imgData, 'PNG', 0, 0, imgWidth, imgHeight)
pdf.save(filename)
}
// Vue组件中使用
methods: {
async exportPage() {
const element = this.$refs.pdfContent
await exportHTMLToPDF(element, 'page_export.pdf', {
scale: 3 // 更高分辨率
})
}
}
高级功能示例:
async function exportMultiPagePDF() {
const elements = document.querySelectorAll('.report-page')
const pdf = new jsPDF('p', 'mm', 'a4')
for (let i = 0; i < elements.length; i++) {
const element = elements[i]
const canvas = await html2canvas(element, {
scale: 2,
logging: true
})
const imgData = canvas.toDataURL('image/png')
const imgWidth = pdf.internal.pageSize.getWidth()
const imgHeight = (canvas.height * imgWidth) / canvas.width
if (i > 0) {
pdf.addPage()
}
pdf.addImage(imgData, 'PNG', 0, 0, imgWidth, imgHeight)
// 添加页脚
pdf.setFontSize(10)
pdf.setTextColor(150)
pdf.text(
`第 ${i + 1} 页`,
pdf.internal.pageSize.getWidth() / 2,
pdf.internal.pageSize.getHeight() - 10,
{ align: 'center' }
)
}
pdf.save('multi_page_report.pdf')
}
优点:
缺点:
pdfmake是一个声明式的PDF生成库,使用JSON格式定义文档结构。
安装:
npm install pdfmake
基础实现:
import pdfMake from 'pdfmake/build/pdfmake'
import pdfFonts from 'pdfmake/build/vfs_fonts'
pdfMake.vfs = pdfFonts.pdfMake.vfs
export function generatePDFWithPdfMake(data, filename = 'document.pdf') {
const docDefinition = {
content: [
{ text: '销售报表', style: 'header' },
'\n\n',
{
table: {
headerRows: 1,
widths: ['*', 'auto', 'auto', 'auto'],
body: [
['产品', '季度1', '季度2', '季度3'],
...data.map(item => [
item.product,
{ text: item.q1, alignment: 'right' },
{ text: item.q2, alignment: 'right' },
{ text: item.q3, alignment: 'right' }
])
]
}
}
],
styles: {
header: {
fontSize: 18,
bold: true,
alignment: 'center'
}
},
defaultStyle: {
font: 'SimSun'
}
}
pdfMake.createPdf(docDefinition).download(filename)
}
// Vue组件中使用
methods: {
exportData() {
const data = [
{ product: '产品A', q1: 1000, q2: 1500, q3: 1200 },
{ product: '产品B', q1: 800, q2: 900, q3: 1000 }
]
generatePDFWithPdfMake(data, 'sales_report.pdf')
}
}
高级功能示例:
function generateAdvancedPdfMakeDocument() {
const docDefinition = {
pageSize: 'A4',
pageMargins: [40, 60, 40, 60],
header: function(currentPage, pageCount) {
return {
text: `第 ${currentPage} 页 / 共 ${pageCount} 页`,
alignment: 'right',
margin: [0, 20, 20, 0]
}
},
footer: function(currentPage, pageCount) {
return {
text: '公司机密 - 未经授权禁止复制',
alignment: 'center',
fontSize: 8,
margin: [0, 0, 0, 20]
}
},
content: [
{
columns: [
{
width: 100,
image: 'logo',
fit: [80, 80]
},
{
width: '*',
text: '2023年度财务报告',
style: 'reportTitle'
}
]
},
'\n\n',
{
text: '1. 销售概览',
style: 'sectionHeader'
},
{
text: '本年度公司整体销售额达到¥1,200万,同比增长15%。主要增长来自产品线A和B。',
style: 'paragraph'
},
'\n',
{
style: 'tableExample',
table: {
widths: ['*', '*', '*', '*'],
body: [
[
{ text: '产品线', style: 'tableHeader' },
{ text: 'Q1', style: 'tableHeader' },
{ text: 'Q2', style: 'tableHeader' },
{ text: 'Q3', style: 'tableHeader' }
],
['产品A', '320万', '350万', '380万'],
['产品B', '280万', '300万', '320万'],
['产品C', '150万', '160万', '170万'],
[
{ text: '总计', style: 'tableHeader' },
{ text: '750万', colSpan: 3, style: 'tableHeader' },
{},
{}
]
]
}
},
'\n\n',
{
text: '2. 成本分析',
style: 'sectionHeader',
pageBreak: 'before'
},
{
canvas: [
{
type: 'rect',
x: 0, y: 0,
w: 500, h: 20,
color: '#eeeeee'
},
{
type: 'rect',
x: 0, y: 0,
w: 350, h: 20,
color: '#cc0000'
}
]
},
{
text: '成本构成示意图',
alignment: 'center',
italics: true,
fontSize: 10,
margin: [0, 5, 0, 0]
}
],
styles: {
reportTitle: {
fontSize: 24,
bold: true,
alignment: 'center',
margin: [0, 20, 0, 0]
},
sectionHeader: {
fontSize: 16,
bold: true,
margin: [0, 15, 0, 5]
},
paragraph: {
fontSize: 12,
lineHeight: 1.5,
margin: [0, 0, 0, 10]
},
tableExample: {
margin: [0, 5, 0, 15]
},
tableHeader: {
bold: true,
fontSize: 13,
color: 'black',
fillColor: '#dddddd'
}
},
images: {
logo: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAA...'
}
}
pdfMake.createPdf(docDefinition).open()
}
优点:
缺点:
vue-pdf是一个Vue专用的PDF生成和显示组件库。
安装:
npm install @tato30/vue-pdf
基础实现:
员工信息
ID
姓名
部门
{{ emp.id }}
{{ emp.name }}
{{ emp.department }}
优点:
缺点:
这种方案将PDF生成逻辑放在服务器端,前端只负责触发和下载。
前端代码:
export function requestPDFExport(params) {
return axios({
url: '/api/export-pdf',
method: 'POST',
data: params,
responseType: 'blob'
}).then(response => {
const blob = new Blob([response.data], {
type: 'application/pdf'
})
const url = window.URL.createObjectURL(blob)
const link = document.createElement('a')
link.href = url
link.setAttribute('download', 'server_export.pdf')
document.body.appendChild(link)
link.click()
document.body.removeChild(link)
})
}
// Vue组件中使用
methods: {
async exportFromServer() {
try {
this.loading = true
await requestPDFExport({
reportType: 'sales',
year: 2023,
format: 'A4'
})
} catch (error) {
console.error('导出失败:', error)
} finally {
this.loading = false
}
}
}
Node.js服务器端示例(使用pdfkit):
const express = require('express')
const PDFDocument = require('pdfkit')
const app = express()
app.post('/api/export-pdf', async (req, res) => {
try {
const { reportType, year, format } = req.body
// 创建PDF文档
const doc = new PDFDocument({ size: format || 'A4' })
// 设置响应头
res.setHeader('Content-Type', 'application/pdf')
res.setHeader('Content-Disposition', 'attachment; filename="report.pdf"')
// 管道传输到响应
doc.pipe(res)
// 添加内容
doc.fontSize(25).text(`${year}年度${getReportTitle(reportType)}`, {
align: 'center'
})
doc.moveDown()
doc.fontSize(12).text('生成日期: ' + new Date().toLocaleDateString())
// 添加表格数据
const data = await getReportData(reportType, year)
drawTable(doc, data)
// 结束并发送
doc.end()
} catch (error) {
console.error('PDF生成错误:', error)
res.status(500).send('PDF生成失败')
}
})
function drawTable(doc, data) {
const startY = 150
const rowHeight = 30
const colWidth = 150
const headers = ['产品', 'Q1', 'Q2', 'Q3', 'Q4', '总计']
// 表头
doc.font('Helvetica-Bold')
headers.forEach((header, i) => {
doc.text(header, 50 + i * colWidth, startY, {
width: colWidth,
align: 'center'
})
})
// 表格内容
doc.font('Helvetica')
data.forEach((row, rowIndex) => {
const y = startY + (rowIndex + 1) * rowHeight
// 绘制行背景
if (rowIndex % 2 === 0) {
doc.fillColor('#f5f5f5')
.rect(50, y - 10, colWidth * headers.length, rowHeight)
.fill()
doc.fillColor('black')
}
// 绘制单元格文本
Object.values(row).forEach((value, colIndex) => {
doc.text(String(value), 50 + colIndex * colWidth, y, {
width: colWidth,
align: colIndex > 0 ? 'right' : 'left'
})
})
})
}
app.listen(3000, () => console.log('Server running on port 3000'))
优点:
缺点:
对于需要精确复制网页样式的场景,可以在服务器端使用无头浏览器(如Puppeteer)生成PDF。
Node.js服务器示例(使用Puppeteer):
const express = require('express')
const puppeteer = require('puppeteer')
const app = express()
app.post('/api/export-pdf', async (req, res) => {
let browser = null
try {
const { url, options } = req.body
browser = await puppeteer.launch({
headless: true,
args: ['--no-sandbox', '--disable-setuid-sandbox']
})
const page = await browser.newPage()
// 如果是URL,直接导航;如果是HTML内容,设置内容
if (url.startsWith('http')) {
await page.goto(url, { waitUntil: 'networkidle2' })
} else {
await page.setContent(url)
}
// 生成PDF
const pdf = await page.pdf({
format: 'A4',
printBackground: true,
margin: {
top: '20mm',
right: '20mm',
bottom: '20mm',
left: '20mm'
},
...options
})
// 发送PDF
res.setHeader('Content-Type', 'application/pdf')
res.setHeader('Content-Disposition', 'attachment; filename="export.pdf"')
res.send(pdf)
} catch (error) {
console.error('PDF生成错误:', error)
res.status(500).send('PDF生成失败')
} finally {
if (browser) {
await browser.close()
}
}
})
app.listen(3000, () => console.log('Server running on port 3000'))
优点:
缺点:
方法 | 生成方式 | 样式保真度 | 复杂度 | 性能 | 适用场景 |
---|---|---|---|---|---|
浏览器打印 | 客户端 | 中等 | 低 | 高 | 简单打印需求 |
jsPDF | 客户端 | 低 | 中 | 高 | 编程式生成简单PDF |
html2canvas+jsPDF | 客户端 | 高 | 中 | 中 | 精确复制页面样式 |
pdfmake | 客户端 | 中 | 中 | 中 | 结构化文档生成 |
vue-pdf | 客户端 | 低 | 低 | 高 | 简单Vue集成 |
服务器生成 | 服务端 | 可高可低 | 高 | 依赖实现 | 复杂/大数据量PDF |
无头浏览器 | 服务端 | 极高 | 很高 | 低 | 精确复制复杂页面 |