纯前端Vue3项目PDF导出
前置条件
在当前的Web应用开发中,将页面渲染的内容导出为PDF文件,供用户下载与查看,已成为一项常见且复杂的需求
尤其是当项目规模较大、用户群体庞大时,后端渲染PDF并提供下载链接的方式虽可行,但将消耗大量的服务器资源
因此,寻找一种纯前端实现PDF渲染的解决方案成为了开发中的一大挑战
本文旨在探讨在Vue3框架下,如何利用前端技术实现高效且用户友好的PDF导出功能
项目需求概述
在本项目中,我们面对如下需求:
技术栈
使用Vue3构建的Web应用
用户需求
用户需求能够在页面上查看各类图表,并提供一键导出功能,将图表渲染为PDF文件,以便用户下载和查看
挑战
不增加后端负担
考虑到项目的庞大规模,要求PDF渲染工作完全在前端完成,避免由后端渲染PDF而增加服务器的计算与资源开销
前端实现
需要对前端技术方案进行调研,寻找合适的工具与库来实现HTML元素到PDF的转换,同时确保功能的完整性(包括下载、分页及完整节点渲染等)
技术方案
在进行技术选型和调研过程中,我们决定采用html2canvas
和pdf.js
这两个库作为主要技术方案
以下是具体的实施步骤:
使用html2canvas捕获页面内容
html2canvas`是一个强大的JavaScript库,它能够将HTML元素“截图”并以Canvas的形式返回
这一过程完全基于前端完成,无需服务器参与
使用该库,我们可以将Vue组件渲染的各类图表转换为Canvas对象
1
2
3html2canvas(document.querySelector("#chart")).then(canvas => {
// 操作canvas
});使用pdf.js生成PDF文件
随后,我们利用
pdf.js
库将得到的Canvas内容插入到PDF文件中,并实现分页、排版等功能,最终生成供用户下载的PDF文件pdf.js`提供了丰富的API来处理PDF生成的各种细节,包括分页控制、内容排版等
1
2
3
4
5const imgData = canvas.toDataURL('image/png');
const pdf = new jsPDF();
pdf.addImage(imgData, 'PNG', 0, 0);
pdf.save('download.pdf');全面展示
对于那些未直接在浏览器窗口中展现的内容,我们通过适当的DOM操作和排版逻辑,确保所有相关节点都能在最终的PDF文件中得到准确、完整的展示
代码实现
实现步骤
导入库
1
2import html2canvas from "html2canvas";
import jsPDF from "jspdf";html2canvas
这个库用于将HTML元素渲染为Canvas
Canvas是一种HTML元素,可以用来绘制图形和图像
jsPDF
这个库用于生成PDF文件
它提供了多种方法来操作PDF文档,比如添加文本、图像等
定义常量
1
2const A4_HEIGHT = 841.89; // A4纸高度
const A4_WIDTH = 592.28; // A4纸宽度A4_HEIGHT 和 A4_WIDTH
定义了A4纸张的标准尺寸,以点(pt)为单位
这些常量将在后续代码中用于计算和设置PDF页面的尺寸
将HTML元素转换为Canvas
1
2
3
4
5
6
7
8
9
10const toCanvas = async (element, width) => {
const canvas = await html2canvas(element, {
scale: window.devicePixelRatio * 3,
});
const canvasWidth = canvas.width;
const canvasHeight = canvas.height;
const height = (width / canvasWidth) * canvasHeight;
const canvasData = canvas.toDataURL('image/jpeg', 1.0);
return { width, height, data: canvasData };
};- element:需要转换的HTML元素
- width:目标Canvas的宽度
- html2canvas(element, { scale: window.devicePixelRatio * 3 }):将HTML元素转换为Canvas。
scale
参数提高了图像的清晰度 - canvas.width 和 canvas.height:获取Canvas的宽高
- height = (width / canvasWidth) * canvasHeight:计算目标Canvas的高度,以保持宽高比
- canvas.toDataURL(‘image/jpeg’, 1.0):将Canvas转换为JPEG格式的Base64字符串
- return { width, height, data: canvasData }:返回包含Canvas数据和尺寸的对象
计算分页信息
参数
- nodes:需要计算的HTML节点列表
- rate:节点尺寸与目标宽度的比例
- originalPageHeight:页面原始高度
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15const getPages = (nodes, rate, originalPageHeight) => {
let totalHeight = 0;
const pages = [0];
nodes.forEach((node, index) => {
const top = rate * node.offsetTop;
const height = rate * node.offsetHeight;
const marginTop = index === 0 ? Math.max(top, 0) : 0;
if ((top + height - marginTop) > (totalHeight + originalPageHeight)) {
pages.push(top - marginTop);
totalHeight += (top - marginTop);
}
});
return pages;
};内部逻辑
- totalHeight:记录当前页的总高度
- pages:记录每页的起始位置,初始值为0
- nodes.forEach((node, index) => { … }):遍历每个节点
- top:节点的顶部位置,按比例缩放
- height:节点的高度,按比例缩放
- marginTop:第一个节点的顶部边距
- if ((top + height - marginTop) > (totalHeight + originalPageHeight)):如果节点超出了当前页面,添加新的页面
- pages.push(top - marginTop):记录新页的起始位置
- totalHeight += (top - marginTop):更新当前页的总高度
导出PDF
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25export async function outputPDF(element, title, contentWidth = 550) {
if (!(element instanceof HTMLElement)) return;
const pdf = new jsPDF({ unit: 'pt', format: 'a4', orientation: 'p' });
const { width, height, data } = await toCanvas(element, contentWidth);
const baseX = (A4_WIDTH - contentWidth) / 2;
const baseY = 20;
const originalPageHeight = A4_HEIGHT - (baseY * 2);
const rate = contentWidth / element.offsetWidth;
const pages = getPages(element.childNodes, rate, originalPageHeight);
pages.forEach((page, i) => {
pdf.addImage(data, 'JPEG', baseX, baseY - page, width, height);
pdf.setFillColor(255, 255, 255);
pdf.rect(0, 0, Math.ceil(A4_WIDTH), Math.ceil(baseY), 'F');
if (i < pages.length - 1) {
const imageHeight = pages[i + 1] - page;
pdf.rect(0, baseY + imageHeight, Math.ceil(A4_WIDTH), Math.ceil(A4_HEIGHT - imageHeight), 'F');
pdf.addPage();
}
});
return pdf.save(`${title}.pdf`);
}- if (!(element instanceof HTMLElement)) return;:首先检查传入的元素是否是一个有效的HTML元素,如果不是则直接返回
- const pdf = new jsPDF({ unit: ‘pt’, format: ‘a4’, orientation: ‘p’ });:初始化一个新的PDF文档
- unit:单位设置为点(pt)
- format:页面格式设置为A4
- orientation:页面方向设置为纵向(portrait)
- const { width, height, data } = await toCanvas(element, contentWidth);:调用
toCanvas
函数,将HTML元素转换为Canvas,并获取其数据和尺寸 - const baseX = (A4_WIDTH - contentWidth) / 2;:计算PDF内容的起始X坐标,以使内容居中。
- const baseY = 20;:设置PDF内容的起始Y坐标
- const originalPageHeight = A4_HEIGHT - (baseY * 2);:计算页面的可用高度,减去上下边距
- const rate = contentWidth / element.offsetWidth;:计算节点尺寸与目标宽度的比例
- const pages = getPages(element.childNodes, rate, originalPageHeight);:调用
getPages
函数,获取分页信息
处理每个页面
1
2
3
4
5
6
7
8
9
10
11pages.forEach((page, i) => {
pdf.addImage(data, 'JPEG', baseX, baseY - page, width, height);
pdf.setFillColor(255, 255, 255);
pdf.rect(0, 0, Math.ceil(A4_WIDTH), Math.ceil(baseY), 'F');
if (i < pages.length - 1) {
const imageHeight = pages[i + 1] - page;
pdf.rect(0, baseY + imageHeight, Math.ceil(A4_WIDTH), Math.ceil(A4_HEIGHT - imageHeight), 'F');
pdf.addPage();
}
});- pages.forEach((page, i) => { … }):遍历每个页面
- pdf.addImage(data, ‘JPEG’, baseX, baseY - page, width, height);:将Canvas图像添加到PDF的当前页面
- data:Canvas图像的Base64数据
- baseX:图像的X坐标
- baseY - page:图像的Y坐标,减去当前页的起始位置
- width:图像的宽度
- height:图像的高度
- pdf.setFillColor(255, 255, 255);:设置填充颜色为白色
- pdf.rect(0, 0, Math.ceil(A4_WIDTH), Math.ceil(baseY), ‘F’);:在页面顶部绘制一个白色矩形,作为页眉
- if (i < pages.length - 1):如果不是最后一页,处理下一页
- const imageHeight = pages[i + 1] - page;:计算当前页的图像高度
- pdf.rect(0, baseY + imageHeight, Math.ceil(A4_WIDTH), Math.ceil(A4_HEIGHT - imageHeight), ‘F’);:在页面底部绘制一个白色矩形,作为页脚
- pdf.addPage();:添加新的一页
- pdf.addImage(data, ‘JPEG’, baseX, baseY - page, width, height);:将Canvas图像添加到PDF的当前页面
- pages.forEach((page, i) => { … }):遍历每个页面
保存PDF
1
return pdf.save(`${title}.pdf`);
- pdf.save(
${title}.pdf
);:将PDF文档保存为指定标题的文件
- pdf.save(
源码
1 | /** |
思路讲解
- 导入必要的库:使用
html2canvas
将 HTML 元素转换为 Canvas,使用jsPDF
生成 PDF 文件 - 定义A4纸张的标准尺寸:设置 A4 纸张的宽度和高度
- 将HTML元素转换为Canvas:使用
toCanvas
函数将 HTML 元素转换为 Canvas,并获取其数据和尺寸 - 计算分页信息:使用
getPages
函数计算内容在 PDF 中的分页信息 - 生成PDF文件
- 初始化一个新的 PDF 文档
- 遍历每一页,将 Canvas 图像添加到 PDF 中
- 为每一页添加页眉和页脚
- 保存生成的 PDF 文件