需求:
应广大后端同事需求,将下载pdf功能放在前端实现。后来在思考实现到什么程度的时候想干脆一步到位,前后端分离,跨平台使用。后端只要提供接口,其他的全部交给前端来做。当然也存在一定的问题,比如说安全性,好在公司要实现的东西都是内部在使用。所以撸起袖子就开干了。
一、产品需求
- table被切割时,另起一页。
- 生成的每一页pdf不被页眉页脚覆盖。
- 一个table超过一页pdf,则跨行的那一个tr另起一页
二、前期技术选型
三、方案确定
-
前后端分离
-
前端实现预览下载pdf功能,后端提供接口
-
独立部署,不依赖于任何一个已有项目。可供公司跨平台项目使用
四、前端插件
- html2canvas 使用JavaScript屏幕截图
- jspdf 用JavaScript生成pdf的库
- jsrender 模板库,它具有无代码标记语法和高性能,既不依赖jQuery,也不依赖文档对象模型(documentobjectmodel,DOM),支持创建自定义函数,并使用纯基于字符串的呈现。
五、html2canvas+jspdf(踩坑)
插件自身短板许多问题,大量查阅资料得到了解决。也有曲线救国方案(文字图片被分割,通过计算动态添加删除padding)。
1. 下载pdf背景黑色(期望:#FFFFFF)
html2canvas配置添加background: #FFFFFF
var element = $("#demo");
html2canvas(element,{background :'#FFFFFF'}).then(function(canvas) {
2. 像素失真模糊
并引入含有这两项配置的js文件jspdf
var ele = $("#demo");
html2canvas(ele,{background :'#FFFFFF', scale:2, dpi: 150}).then(function(canvas){})
3. 水印
文字水印
function addWaterMark(pdf) {
var totalPages = pdf.internal.getNumberOfPages();
for (i = 1; i <= totalPages; i++) {
pdf.setPage(i);
pdf.setTextColor(150);
pdf.text(50, doc.internal.pageSize.height - 30, 'Watermark');
}
return pdf;
}
图片水印
// 添加图片水印
var totalPages = pdf.internal.getNumberOfPages();
var img = new Image;
img.crossOrigin = ""; // for demo as we are at different origin than image
img.src = './logo-img1.png'; // some random imgur image
img.onload = function() {
for (i = 1; i <= totalPages; i++) {
pdf.setPage(i);
pdf.addImage(img, 520, 0);
}
pdf.save('order.pdf');
};
4. 页眉页脚(同水印文字)
5. 分页text、table、img被切割
解决问题的设计思想:(与业务强关联)
计算元素:tr
计算方式:(这里我们默认最小单位tr高度不超过pdf一页高度)
- tr自身高度超过pdf一页高度,先另起一页。再向下找小单位的tr进行递归算法。
- tr自身高度不超过pdf一页高度。但是正好跨页或者到当前页底部的距离不够展示页脚。需要另起一页。
计算收尾:下载后清除预览的padding
技术实现:
两种情况需要分页(当前tr下的所有td添加padding-top)
- tr高度 > pdf height
- tr高度 < pdf height,但是tr top 到 tr top所在页的距离 - tr自高 < 页脚pageFooterH(即tr跨页或者tr 到当前页底部距离< 页脚)
页眉页脚及水印不影响table跨页问题:将页眉页脚做成了两张白色背景的width: 100%的图片加入pdf
异步操作dom及下载:由于存在递归循环操作dom,以及等待dom被操作(tt)后才可下载pdf,运用了async/await完美解决
注意:编码html的时候不给td添加任何的padding-top,为后期下载pdf做预留。为了实现td中添加padding,可给td中元素包一个父元素设置样式。
6. html2canvas无法绘制图片
img元素地址跨域,可将图片添加到本地访问。
// 不显示
<img src="https:baidu.com/public/logo.png">
// 显示
<img src="../public/logo.png">
六、jsrender js模版引擎
在react、vue横行的时代。早期的模版引擎基本上没有什么用武之地了。这次的项目因为要实现跨平台前后端分离。所以启用了在github上还比较活跃的jsrender来实现。
1. 结构
<div class="content-body"></div>
<script type="text/x-jsrender" id="j-parent">
<table border="0" cellspacing="0" cellpadding="0" id="pdf-content">
<tbody>
<tr>
<td>123</td>
</tr>
</tbody>
</table>
</script>
<script src="../jquery-3.5.1.min.js"></script>
<script src="../jsrender.min.js"></script>
<script type="text/javascript">
const data = {
date: '20210127',
firstName: 'yang',
lastName: 'yd',
number: 27,
cat: '<span>mimi<span>',
skill: ['javascript','html','vue'],
}
//获取模板
jsRenderTpl = $.templates('#j-order-pdf'),
//模板与数据结合
finalTpl = jsRenderTpl(data);
document.getElementsByClassName('content-body')[0].innerHTML = finalTpl
</script>
2. 基本用法
{{:}}
和 {{>}}
(或{{html:}}
)两者都可以输出内容,不过后者是经过html
编码的。
<p>a cat {{:cat}}</p> // a mimi
<p>a cat {{>cat}}</p> // a <span>mimi</span>
<p>a cat {{html:cat}}</p> // a <span>mimi</span>
3. 判断if/else
语法:{{if condition}} ... {else condition} ... {{else}}... {{/if}}
<p>
{{if age < 6}}
baby
{{else age > 50}}
old man
{{else}}
youth
{{/if}}
<p>
// youth
4. 循环for
语法: {{for}} ... {{/for}}
<p>
{{for skill itemVar="~item"}}
<span>{{:~item}}</span>
{{/for}}
</p>
// javascript html vue
其中~item当前循环的值的别名。
5. 模版:include组合模版
语法:include tmpl="模板id"
<script type="text/x-jsrender" id="j-parent">
{{include {"item": ~item} tmpl="#j-child-template"/}}
</script>
<script type="text/x-jsrender" id="j-child-template">
<p>我被include了</p>
</script>
// 我被include
6. views.helpers
语法:
-
视图 {{~"标签名称"(附加参数)}}
-
逻辑 $.views.helpers({"标签名称":function(参数){code}})
7. views.converters 转换器
语法:
-
视图 {{“转化器名称”:参数}}
-
js .views.converters({"转换器名称":function(参数){code}})
name: {{:~replaceName(firstName, lastName)}}
七、安全
当项目搬来前端实现基本上已经放弃了安全这部分的考虑……^_^,由于是公司内部使用,所以前端来实现了这个功能。在路由上给每一个id添加了加密,至少可以防止通过修改链接访问数据。
总结:
由于以前都是后端负责下载pdf这一块。后端同事每次修改样式都痛不欲生。这一次也是为了解决后端同事的三千烦恼做了一次大的改革。实现了跨平台,公司的任何有关下载pdf项目都可以使用这一套来实现。
开始的技术选型很重要,条条大路通罗马。也许有更好的方式来实现。