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分页

性能优化

大文件考虑流式生成,避免浏览器卡顿

← Chrome Console Tips