一、jsPDF项目简介
最近遇到个需要将网页中特定内容转为PDF的需求,所以有机会试用了下jsPDF,也遇到了一些问题,这里分享给大家了。
首先,项目地址:https://github.com/parallax/jsPDF
目前2.6万的star数,可以说是Github上Top级别的项目了,也是Web导PDF的首选解决方案。
官方的使用示意也很简单,构造,内容和保持。
import { jsPDF } from "jspdf";
// 默认是 a4 纸张尺寸,纵向,单位是mm
const doc = new jsPDF();
doc.text("Hello world!", 10, 10);
doc.save("a4.pdf");
好,接下来,就是把页面中的DOM内容作为PDF内容就可以了,理论上如此,但实际使用却令人深思。
二、内置html()方法惊为天人
jsPDF内置一个名为html()的方法,可以直接让HTML元素作为PDF的内容。
var doc = new jsPDF();
// element就是需要转变为pdf的DOM元素
doc.html(element, {
callback: function (doc) {
doc.save();
},
x: 10,
y: 10
});
结果我试了下,效果惊为天人。
研究一番,发现是要导入完整的中文字体,而一个中文字体,少说5-6M,在Web这种场景下就不太合适。
于是改变策略,决定先用html2canvas将内容转成图片,再以图片的方式,一页一页地插入到PDF中,实现成本会低很多,不足就是文字无法选择。
三、借助html2canvas
html2canvas这个项目之前也有提过,接近3万 Star,也是Github上的顶级开源项目。
项目地址:https://github.com/niklasvh/html2canvas
极简使用示意
这里快速演示下如何使用html2canvas和jsPDF生成PDF文件。
假设页面上有个布局元素,id属性值是element,效果如下图所示:
则下面的这段代码就可以让这段布局内容变成PDF文件下载下来。
<script src="./html2canvas.min.js"></script>
<script src="./jspdf.umd.min.js"></script>
<script>
var pdf = new jspdf.jsPDF();
html2canvas(target).then(function(canvas) {
pdf.addImage(canvas.toDataURL('image/jpeg'), 10, 10);
pdf.save('mybook.pdf');
});
</script>
此时的PDF打开效果就是这样的:
眼见为实,您可以狠狠地点击这里:html2canvs与jspdf生成PDF简易demo
然而,真实的项目开发要比demo页面麻烦的多。
几乎大多数人一定会遇到的问题,那就是如果html2canvas生成的图片过长,该如何在PDF中分页。
以及,图片跨域了,又当如何处理?
四、图片跨域和分页的问题
canvas与图片跨域的问题,我之前专门撰文讲解过,传送门地址:"解决canvas图片getImageData,toDataURL跨域问题"
里面的方法虽多,根据我多年的实践,还是服务器设置Access-Control-Allow-Origin
运行访问,前端fetch或XMLHttpRequest获取图片数据的策略最好用。
以fetch举例,想要获得一个图片地址是 imgUrl 的图像数据,可以这么处理:
fetch(imgUrl).then(res => res.blob()).then(blob => {
var reader = new FileReader() ;
reader.onload = function () {
// this.result 就是图片的base64地址
};
reader.readAsDataURL(blob) ;
})
为何需要转成base64呢?因为根据我的实践,html2canvas内容中的图片地址,需要转换成base64地址,才能真正可用。
分页的问题
分页的问题可以使用代码搞定,设置好每一页PDF的高度,然后canvas的高度一页一页剪掉再分别添加即可。
这里就有一些需要提前知道的,关于尺寸的知识。
当我们构造一个 pdf 实例的时候,如果不设置任何参数,则其尺寸是 A4 纸的尺寸,210毫米×297毫米。
根据我的实践,这个尺寸有些小了,生成的PDF内容模糊,阅读体验极为不佳。
所以,有必要对PDF尺寸进行自定义,也就是设置得大一些,虽然大尺寸让PDF文件占据空间也大了,但现在是大屏高清时代,流量不值钱,此不足不值一提。
具体而言,我是这么处理的。
首先确定好页面中容器元素的宽度,假设是700px,则PDF的尺寸可以设置为2倍,也就是1400px,而竖版PDF的高宽比是根号二,也就是1.414,所以PDF的高度就是1400*1.414=1979.6像素。
此时,再配合简单的数学计算,我们就可以将canvas图像分隔成一页一页的,分别塞在PDF中。
具体代码参见下一节封装的代码示意。
五、封装后的的方法
为了方便遇到类似需求的同学可以快速完成对应的开发。
我将上面的图像处理和分页封装成了可复用的方法,代码如下所示(支持jspdf和html2canvas src直连和 npm install 安装两种使用形式):
// 导出 pdf 封装方法
// by zhangxinxu(.com)
// 访问 https://www.zhangxinxu.com/wordpress/?p=10854 了解更新信息
export async function exportPdf (element, filename = '未命名', callback = () => {}) {
if (!element) {
callback();
return;
}
// 尺寸的确定
const originWidth = element.offsetWidth || 700;
// 创建一个容器,用于克隆元素
const container = document.createElement('div');
// 16px是为了生成的PDF有安全边距
container.style.cssText = `position:fixed;left: ${-2 * originWidth}px; top:0;padding:16px;width:${originWidth}px;box-sizing:content-box;`;
// 插入到body中
document.body.appendChild(container);
// 克隆元素
container.appendChild(element.cloneNode(true));
// 依赖的库
var jsPDF;
if (typeof html2canvas == 'undefined') {
html2canvas = await import('html2canvas').then(module => module.default);
}
if (typeof jspdf == 'undefined') {
jsPDF = await import('jspdf').then(module => module.jsPDF);
} else {
jsPDF = jspdf.jsPDF;
}
// 为了保证显示质量,2倍PDF尺寸
const scale = 2;
const width = originWidth + 32;
const PDF_WIDTH = width * scale;
const PDF_HEIGHT = width * 1.414 * scale;
// 渲染方法
const render = function () {
// 渲染为图片并下载
html2canvas(container, {
scale: scale
}).then(function(canvas) {
const contentWidth = canvas.width;
const contentHeight = canvas.height;
` // 一页pdf显示html页面生成的canvas高度
const pageHeight = contentWidth / PDF_WIDTH * PDF_HEIGHT;
// canvas图像在画布上的尺寸
const imgWidth = PDF_WIDTH;
const imgHeight = PDF_WIDTH / contentWidth * contentHeight;
let leftHeight = contentHeight;
let position = 0;
const doc = new jsPDF('p', 'px', [PDF_WIDTH, PDF_HEIGHT]);
// 不足一页
if (leftHeight < pageHeight) {
doc.addImage(canvas, 'PNG', 0, 0, imgWidth, imgHeight);
} else {
// 多页
while (leftHeight > 0) {
doc.addImage(canvas, 'PNG', 0, position, imgWidth, imgHeight)
leftHeight -= pageHeight;
position -= PDF_HEIGHT;
//避免添加空白页
if (leftHeight > 0) {
doc.addPage();
}
}
}
doc.save(filename + '.pdf');
// 移除创建的元素
container.remove();
// 隐藏全局loading提示
callback();
});
`
}
// 图像地址替换成base64地址
const eleImgs = container.querySelectorAll('img');
const length = eleImgs.length;
let start = 0;
container.querySelectorAll('img').forEach(ele => {
let src = ele.src;
`if (!src) {
return;
}
// 事件处理,必须成功或失败
ele.onload = function () {
if (!/^http/.test(ele.src)) {
start++;
if (start == length) {
render();
}
}
};
// 请求图片并转为base64地址
fetch(src).then(res => res.blob()).then(blob => {
var reader = new FileReader() ;
reader.onload = function () {
ele.src = this.result;
};
reader.readAsDataURL(blob) ;
}).catch(() => {
// 请求异常处理
start++;
if (start == length) {
render();
}
});
`
});
}
可以自动将容器元素中的图片Base64,同时内容分布在每一页的PDF上并下载。
实践出真知
为了验证封装的方法的效果,我特意做了个演示页面。
//zxx: 演示页面采用的是直联调用
您可以狠狠地点击这里:exportPdf封装方法与跨域图片PDF导出demo
打击可以点击下图所示的"PDF生成"按钮,菊花转几圈之后,就可以看到PDF下载的提示了(看你浏览器设置,也可能是直接保持到本地)。
下图是生成的PDF文件的缩略图,可以看到图片和排版都是完全符合预期的。
调用这块的JS代码参考:
<script src="./html2canvas.min.js"></script>
<script src="./jspdf.umd.min.js"></script>
<script type="module">
import { exportPdf } from './exportPdf.js';
// 点击按钮执行PDF导出
button.addEventListener('click', () => {
const article = document.querySelector('article');
// 显示loading
button.loading = true;
// 由于导出PDF是异步的,所以需要在导出完成后隐藏loading
exportPdf(article, '最终章 极北大迷宫', () => {
button.loading = false;
});
});
</script>
少了很多处理细节,是不是实现起来简单多了。
六、跨行内联背景色的渲染问题
实际使用中,还遇到了比较棘手的问题。
就是内联元素,如果有背景色,且这个背景色换行了,则生成的PDF的这部分色块会覆盖部分内容,导致异常。
例如页面渲染是这样的:
PDF效果却是这样的:
我查了下html2canvas的issues,有多个类似反馈,但是都没有进行处理。
按照我对html2canvas底层实现方式的理解,这个问题确实不太好处理。
但是,并不表示没有方法。
可以将整块的内联元素,分隔成一个一个独立的内联元素,这样就可以正常渲染了。
也就是将这个结构:
<span class="bgcolor">CSS新世界</span>
转换成这样子的(实际开发不能换行,这里是为了方便大家阅读刻意处理的):
<span>
<span class="bgcolor">C</span>
<span class="bgcolor">S</span>
<span class="bgcolor">S</span>
<span class="bgcolor">新</span>
<span class="bgcolor">世</span>
<span class="bgcolor">届</span>
</span>
来看下最终的效果。
您可以狠狠地点击这里:解决html2canvas span inline background色块问题demo
点击下面这个按钮,JS会对原来的DOM结构进行处理(实际开发可以克隆该元素再处理,以避免DOM结构变化会带来潜在风险):
此时,生成的PDF效果就和原始布局样式效果保持一致了:
七、其他点点点
本文示意页面所使用的JS文件都是script直连。
实际开发,多是走前端框架。
由于这两个JS都是体积比较大的JS,因此,可以使用动态加载的方式来实现。
import('html2canvas').then(module => module.default).then(...)
import('jspdf').then(module => module.jsPDF).then(...)
盼星星盼月亮
《CSS选择器世界 第2版》的签字版也已经可以购买啦,打开手机淘宝,扫下图左下角的码就可以了。
//zxx: 包邮,另外,我这里还有三张极客时间14天畅学卡,先购买的优先赠送之。
OK,其他就没什么好说的,希望本文的内容可以帮到遇到类似需求的小伙伴。
❤️ ? ? ? ? ?
(本篇完)