html转pdf

3,672 阅读2分钟

需求:

应广大后端同事需求,将下载pdf功能放在前端实现。后来在思考实现到什么程度的时候想干脆一步到位,前后端分离,跨平台使用。后端只要提供接口,其他的全部交给前端来做。当然也存在一定的问题,比如说安全性,好在公司要实现的东西都是内部在使用。所以撸起袖子就开干了。

一、产品需求

  1. table被切割时,另起一页。
  2. 生成的每一页pdf不被页眉页脚覆盖。
  3. 一个table超过一页pdf,则跨行的那一个tr另起一页

二、前期技术选型

三、方案确定

  1. 前后端分离

  2. 前端实现预览下载pdf功能,后端提供接口

  3. 独立部署,不依赖于任何一个已有项目。可供公司跨平台项目使用

四、前端插件

  1. html2canvas 使用JavaScript屏幕截图
  2. jspdf 用JavaScript生成pdf的库
  3. 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项目都可以使用这一套来实现。

开始的技术选型很重要,条条大路通罗马。也许有更好的方式来实现。