前端网页新增导出pdf功能实现方式
前言
本次遇到了这样的一个需求,给部署在vuepress上的文档新增导出功能,本质上就是一个把网页指定内容导出为pdf的需求。
vuepress本身只是一个支持md文档生成静态网页的框架式网站,尝试找过vuepress的插件,发现这方面的插件竟然没有支持网页端在线导出的。
在网上查阅大量资料后,发现许多方式需要编写服务来支持前端的导出pdf功能,而且纯前端实现导出功能,许多都会遇到样式上的问题。
通过半天对各种方式的尝试,对比后找到一种比较简单的方法能满足需求,这种方法使用浏览器自带功能,下面第二张图就是使用window.print()弹出打印页面,选择导出pdf后的效果。
最后导出可复制的pdf:
html2canvas+jsPDF
html2canvas 是一个用于将 HTML 元素转换为 Canvas 的库。它使用 HTML5 的 元素来捕获HTML 元素的内容。这对于将动态内容(如图表、SVG 等)转换为图像非常有用。
jsPDF 是一个用于生成 PDF 文件的库。它提供了许多方法来创建、编辑和保存 PDF 文件。当与 html2canvas 结合使用时,你可以先将 HTML 内容转换为 Canvas,然后使用 jsPDF 将 Canvas 内容导出为 PDF。
网上教程比较多的就是这种html2canvas+jsPDF的方法生成pdf,不过生成的pdf中文档的样式虽然保持住了,但是交互性为0。
这种方式本质上pdf中的文档都是以图片这种方式存在于文档中的,没办法进行复制。
这种适用只在乎布局样式,不适用于生成带交互性的文档。
具体实现如下:
//先引入js库
import html2canvas from 'html2canvas';
import jsPDF from 'jspdf';
//因为是在vue组件里写的方法,有些语句需要自己改下
async exportToPDF() {
try {
// 获取需要导出的元素,.theme-vdoing-wrapper为包含整个文档的class
const element = document.querySelector(".theme-vdoing-wrapper ");
let that = this;
// 导出之前先将滚动条置顶,不然会出现数据不全的现象
window.pageYOffset = 0
document.documentElement.scrollTop = 0
document.body.scrollTop = 0
element.style.background = '#FFFFFF'
html2canvas(element, {
logging: false,
dpi: window.devicePixelRatio * 4, // 将分辨率提高到特定的DPI 提高四倍
scale: 2, // 按比例增加分辨率
element: element,
backgroundColor: '#ffffff',
allowTaint: true,
useCORS: true
}).then(function (canvas) {
var pdf = new jsPDF('p', 'mm', 'a4') // A4纸,纵向
pdf.setFontSize(30) // 字体大小
// pdf.text(20, 30, pdf)
var ctx = canvas.getContext('2d')
var a4w = 170; var a4h = 257 // A4大小,210mm x 297mm,四边各保留20mm的边距,显示区域170x257
var imgHeight = Math.floor(a4h * canvas.width / a4w) // 按A4显示比例换算一页图像的像素高度
var renderedHeight = 0
var options = { pagesplit: true }
while (renderedHeight < canvas.height) {
var page = document.createElement('canvas')
page.width = canvas.width
page.height = Math.min(imgHeight, canvas.height - renderedHeight)// 可能内容不足一页
// 用getImageData剪裁指定区域,并画到前面创建的canvas对象中
page.getContext('2d').putImageData(ctx.getImageData(0, renderedHeight, canvas.width, Math.min(imgHeight, canvas.height - renderedHeight)), 0, 0)
pdf.addImage(page.toDataURL('image/jpeg', 1.0), 'JPEG', 10, 10, a4w, Math.min(a4h, a4w * page.height / page.width)) // 添加图像到页面,保留10mm边距
renderedHeight += imgHeight
if (renderedHeight < canvas.height) { pdf.addPage() }// 如果后面还有内容,添加一个空页
}
var curDate = new Date().toLocaleString("zh-Hans-CN", { year: "numeric", month: "2-digit", day: "2-digit", hour12: false, hour: "2-digit", minute: "2-digit", second: "2-digit" }).replace(/[\/ :]/g, "")
pdf.save(that.exportName + curDate)
that.isLoading = false;
})
} catch (error) {
console.error('导出 PDF 失败:', error);
}
}
导出效果如下(不可复制):
html2pdf
这个是上面方法的简化版,本质上内部也是使用的html2canvas和jsPDF
具体实现:
import html2pdf from 'html2pdf.js';
async exportPDF() {
const element = document.querySelector(".theme-vdoing-wrapper ");
const options = {
margin: 1,
filename: 'output.pdf',//输出文件名
image: { type: 'jpeg', quality: 0.98 },
html2canvas: { scale: 2, letterRendering: true, },
jsPDF: { unit: 'in', format: 'letter', orientation: 'portrait' }
};
try {
const pdf = await html2pdf().from(element).set(options).save();
} catch (err) {
console.error('Error exporting to PDF:', err);
}
},
导出效果如下(不可复制):
window.print()
这种方法是调用浏览器的打印功能,也支持导出pdf,浏览器会考虑网页的 CSS 样式来呈现打印内容,可以解决导出后样式不一致和文本不可复制的问题,成功解决了需求。
具体代码:
printContent() {
window.print();
},
打印过程中想要隐藏元素,需要在css中添加如下内容:
/* 打印样式表 */
@media print {
.hide-on-print {
display: none; /* 隐藏不需要打印的元素 */
}
.article-list{
display: none; /* 隐藏不需要打印的元素 */
}
.footer{
display: none; /* 隐藏不需要打印的元素 */
}
.extra-class::before {
display: none !important; /* 隐藏不需要打印的元素 */
}
}
导出效果如下(可复制):
其他方式
pdfMake(不支持中文要自己装库)没有尝试成功。 PDFKit、Puppeteer、wkhtmltopdf、PhantomJS、Playwright都是在服务器端调用的。
其中尝试了Playwright,最终效果不太好,不支持指定内容导出pdf,导出的是整个网页的。
不过在这里还是附上步骤:
-
先用express搭建一个服务,用于生成pdf
npm init npm i express -D npm i playwright -D npm i cors -D
//在工作目录下新建一个index.js const express = require('express'); const playwright = require('playwright'); const cors = require('cors'); const app = express(); app.use(express.json()); app.use(cors()) app.post('/api/generate-pdf', async (req, res) => { try { const browser = await playwright['chromium'].launch(); // 使用Chromium内核版本的Edge浏览器 const context = await browser.newContext(); const page = await context.newPage(); const { reqUrl } = req.body; await page.goto(reqUrl , { waitUntil: 'domcontentloaded' }); // 确保页面加载完成 // 生成 PDF const pdfOptions ={ format: 'A4', printBackground: true, margin: { top: '1cm', right: '1cm', bottom: '1cm', left: '1cm', }, }; // 直接生成PDF并发送到前端 const pdfBuffer = await page.pdf(pdfOptions); // 设置响应头,告知浏览器这是一个PDF文件 res.set('Content-Type', 'application/pdf'); res.set('Content-Disposition', `attachment;filename=generated.pdf`); // 发送PDF文件内容到前端 res.send(pdfBuffer); // 关闭浏览器实例,释放资源 await browser.close(); } catch (error) { console.error('Error generating PDF:', error); res.status(500).send('Failed to generate PDF'); } }); app.listen(8082, () => { console.log(`Server is running on port 8082`); })
#当前目录下启动服务 node ./index.js
-
前端新增方法通信
核心方法:
//一些配置可以自行百度搜索
async exportToPdf() {
try {
const url = "http://localhost:8082/api/generate-pdf";
const data = {
/* 你的参数对象,比如:{htmlContent: 'your HTML content'} */
reqUrl:'https://blog.csdn.net/qq_42888417/article/details/84317341'
};
// 使用fetch发起POST请求
const response = await fetch(url, {
method: "POST", // 或者 'PUT'
headers: {
"Content-Type": "application/json" // 假设后端期望接收JSON格式的数据
},
body: JSON.stringify(data) // 将数据转换为JSON字符串
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
// 处理响应,直接下载PDF
const blob = await response.blob(); // 获取blob数据
const urlForBlob = window.URL.createObjectURL(blob);
const link = document.createElement("a");
link.href = urlForBlob;
let now = new Date().toLocaleString('en-US', {year: 'numeric', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit', second: '2-digit'}).replace(/[^\\d]/g, '')
link.download = "redis"+now+".pdf";
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
window.URL.revokeObjectURL(urlForBlob); // 清除URL对象,避免内存泄漏
} catch (error) {
// 处理请求错误
console.error("Error generating PDF:", error);
}
}