PDF生成 - 根据后端数据生成PDF
在Web应用中经常需要根据后端数据生成PDF报表,本文介绍前端和后端两种常见的PDF生成方案。
方案对比
| 方案 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| 前端 jsPDF | 无需服务器资源,纯前端处理 | 复杂布局困难,中文支持差 | 简单报表、票据 |
| 前端 html2pdf | 支持CSS布局,简单易用 | 依赖渲染,性能较差 | 样式复杂的页面 |
| 后端 Puppeteer | 渲染准确,支持复杂布局 | 占用服务器资源 | 复杂报表、批量生成 |
| 后端 PDFKit | 精确控制,分块加载 | 代码编写复杂 | 高质量要求场景 |
1. 前端方案 - jsPDF + jspdf-autotable
// 安装: npm install jspdf jspdf-autotable
import { jsPDF } from 'jspdf';
import autoTable from 'jspdf-autotable';
// 获取后端数据
async fetchReportData() {
const res = await fetch('/api/report');
return await res.json();
}
// 生成PDF
async generatePDF() {
const data = await fetchReportData();
const doc = new jsPDF();
// 标题
doc.setFontSize(18);
doc.text('销售报表', 14, 22);
// 日期
doc.setFontSize(11);
doc.text(`生成日期: ${new Date().toLocaleDateString()}`, 14, 30);
// 表格数据
const tableData = data.items.map(item => [
item.date,
item.product,
item.quantity,
item.amount,
item.status
]);
autoTable(doc, {
head: [['日期', '产品', '数量', '金额', '状态']],
body: tableData,
startY: 35,
theme: 'striped',
headStyles: { fillColor: [66, 139, 202] }
});
// 汇总
const finalY = doc.lastAutoTable.finalY + 10;
doc.setFontSize(12);
doc.text(`总金额: ¥${data.total}`, 14, finalY);
// 下载
doc.save('report.pdf');
}
2. 前端方案 - html2pdf.js
// 安装: npm install html2pdf.js
import html2pdf from 'html2pdf.js';
// 创建要打印的HTML模板
function createPDFTemplate(data) {
return `
<div style="padding: 20px; font-family: sans-serif;">
<h1 style="text-align: center;">销售报表</h1>
<p style="color: #666;">生成日期: ${new Date().toLocaleDateString()}</p>
<table style="width: 100%; border-collapse: collapse; margin-top: 20px;">
<thead>
<tr style="background: #f5f5f5;">
<th style="border: 1px solid #ddd; padding: 8px;">日期</th>
<th style="border: 1px solid #ddd; padding: 8px;">产品</th>
<th style="border: 1px solid #ddd; padding: 8px;">数量</th>
<th style="border: 1px solid #ddd; padding: 8px;">金额</th>
</tr>
</thead>
<tbody>
${data.items.map(item => `
<tr>
<td style="border: 1px solid #ddd; padding: 8px;">${item.date}</td>
<td style="border: 1px solid #ddd; padding: 8px;">${item.product}</td>
<td style="border: 1px solid #ddd; padding: 8px;">${item.quantity}</td>
<td style="border: 1px solid #ddd; padding: 8px;">¥${item.amount}</td>
</tr>
`).join('')}
</tbody>
</table>
<div style="margin-top: 20px; font-size: 18px; font-weight: bold;">
总金额: ¥${data.total}
</div>
</div>
`;
}
// 生成PDF
async generatePDF() {
const res = await fetch('/api/report');
const data = await res.json();
const element = document.createElement('div');
element.innerHTML = createPDFTemplate(data);
const opt = {
margin: 10,
filename: 'report.pdf',
image: { type: 'jpeg', quality: 0.98 },
html2canvas: { scale: 2 },
jsPDF: { unit: 'mm', format: 'a4', orientation: 'portrait' }
};
html2pdf().set(opt).from(element).save();
}
3. 后端方案 - Puppeteer (Node.js)
// 安装: npm install puppeteer
const puppeteer = require('puppeteer');
const fs = require('fs');
// API接口 - 生成PDF
app.get('/api/generate-pdf', async (req, res) => {
try {
// 1. 获取后端数据
const data = await fetchReportData();
// 2. 启动浏览器
const browser = await puppeteer.launch({
headless: 'new',
args: ['--no-sandbox', '--disable-setuid-sandbox']
});
const page = await browser.newPage();
// 3. 设置HTML内容
const htmlContent = generateHTML(data);
await page.setContent(htmlContent, { waitUntil: 'networkidle0' });
// 4. 生成PDF
const pdfBuffer = await page.pdf({
format: 'A4',
printBackground: true,
margin: { top: '20mm', bottom: '20mm' }
});
await browser.close();
// 5. 返回PDF
res.setHeader('Content-Type', 'application/pdf');
res.setHeader('Content-Disposition', 'attachment; filename="report.pdf"');
res.send(pdfBuffer);
} catch (error) {
res.status(500).json({ error: error.message });
}
});
// 生成HTML模板函数
function generateHTML(data) {
return `
<!DOCTYPE html>
<html>
<head>
<style>
body { font-family: 'Helvetica Neue', sans-serif; padding: 40px; }
h1 { color: #333; }
table { width: 100%; border-collapse: collapse; margin-top: 20px; }
th, td { border: 1px solid #ddd; padding: 12px; text-align: left; }
th { background: #f5f5f5; }
</style>
</head>
<body>
<h1>销售报表</h1>
<p>生成日期: ${new Date().toLocaleDateString()}</p>
<table>
<thead>
<tr><th>日期</th><th>产品</th><th>数量</th><th>金额</th></tr>
</thead>
<tbody>
${data.items.map(item => `
<tr>
<td>${item.date}</td>
<td>${item.product}</td>
<td>${item.quantity}</td>
<td>¥${item.amount}</td>
</tr>
`).join('')}
</tbody>
</table>
<p><strong>总金额: ¥${data.total}</strong></p>
</body>
</html>
`;
}
4. 前端调用后端PDF接口
// 方法1: 直接下载
async downloadPDF() {
const response = await fetch('/api/generate-pdf');
const blob = await response.blob();
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = 'report.pdf';
a.click();
URL.revokeObjectURL(url);
}
// 方法2: 使用axios并设置responseType
async downloadPDF() {
const response = await axios.get('/api/generate-pdf', {
responseType: 'blob'
});
const url = window.URL.createObjectURL(response.data);
const link = document.createElement('a');
link.href = url;
link.setAttribute('download', 'report.pdf');
document.body.appendChild(link);
link.click();
link.remove();
}
5. 实际例子 - 订单报表生成
订单报表
生成日期: 2024-01-15
| 订单号 | 产品 | 金额 | 状态 |
|---|---|---|---|
| ORD001 | 笔记本电脑 | ¥5,999 | 已完成 |
| ORD002 | 无线鼠标 | ¥199 | 已完成 |
| ORD003 | 机械键盘 | ¥599 | 处理中 |
总金额: ¥6,797
页脚信息 - 公司名称 - 联系电话
PDF生成最佳实践
选择合适方案
简单报表用前端jsPDF,复杂布局用后端Puppeteer
中文字体
后端方案需配置中文字体,前端可用字体子集嵌入
分页处理
长表格需要处理分页,使用autotable或后端HTML分页
性能优化
大文件考虑流式生成,避免浏览器卡顿