DOM 转 PDF 的 5 种方案

项目中想要将侧边弹窗里的内容导出成 PDF. 纯前端手段是否可以完成呢?

一分钟 DEMO

所有的 demo 都用最简便的代码实现, 可以直接控制台执行;


来玩一玩


结论简单明了

欢迎评论更多思路


方案对比

方案概要 css 媒体查询 iframe 打开 DOM 元素 将 body 重置为目标 dom 元素, 打印后还原 body 三方库 jsPDF 三方库 jsPDF + 三方库 html2canvas
原理简述 1. css 媒体查询 print, 可以在打印时隐藏不相关的元素, 将要打印的局部占满全屏, z-index 设置极高 2. window.print 打印可另存为 pdf 1. 创建一个空的 iframe 我们可以完全控制 window 对象和 dom 节点 2. 往 iframe 中插入想要打印的 html 模板 3. 往 iframe 中插入想要打印的 css 样式 4. 触发 iframe 的 window.print 另存为 pdf 1. 将整个 body 的 dom 节点缓存下来 2. 将 body 设置为打印的目标元素 3. 触发 window.print 另存为 pdf 4. 还原 body 的 dom 节点 1. 可以创建 pdf 文件, 然后逐个元素添加至 pdf 文件中; 2. 也可以将 dom 节点转换为 pdf 1. html2canvas 将 dom 节点转换为图片 2. jsPDF 将图片转换为 pdf 文件
优点 - 常规打印样式设置, 可以用于网页 pdf 导出; 也可以稍加调整用于网页局部 pdf 导出; - 改动难度小, 不需要编写 JS 逻辑 - 可以将任意内容导出成 pdf 文档, 而不必是网页上的内容 - 改动非常小, 实现非常快速 - 可以完全掌控 pdf 文件的构造, 逐个段落输出 - 改动非常小, 实现非常快速
缺点 - 没有明显缺点 - 没有明显缺点 - 会丢失 DOM 节点绑点的事件, 大部分场景这思路就不可用 - 打印过程中, 网页内容被改变, 略微影响用户体验 - 目标元素的样式如果和 DOM 层级结构相关将会丢失 - 需要前端引入字体库, 否则就是乱码 (20M 左右), - 直接将 dom 节点转 pdf 效果较差, 丢失大量样式设置 - 丢失 pdf 文档的文本内容, 和直接导出图片没有区别 - html2canvas 库的 bug, 会造成部分样式错误渲染, 比如阴影;
demo demo 1 demo 2 demo 3 demo 4 demo 5

所有的 demo 都用最简便的代码实现, 可以直接控制台执行;

demo 1 css 媒体查询

示例网站: baidu.com

第一步: 在 element 中新增 <style> 标签, 内容如下

xml 复制代码
<style>
    .mnav.c-font-normal.c-color-t {
        color: red;
    }
    @media print {
      .mnav.c-font-normal.c-color-t {
        color: green !important; 
      }
    }
</style>

头部超链接颜色变成了红色

第二步: 调试控制台运行 window.print(), 打印的 PDF 中头部超链接颜色显示为绿色

demo 2 iframe 打开 DOM 元素

示例网站: baidu.com

调试控制台直接执行如下代码:

ini 复制代码
     function printElement(e) {
        var ifr = document.createElement('iframe');
        ifr.style = 'height: 0px; width: 0px; position: absolute'
        document.body.appendChild(ifr);
        ifr.contentDocument.body.appendChild(e.cloneNode(true))
        ifr.contentWindow.print();

        ifr.parentElement.removeChild(ifr);
    }
    var style = document.createElement('style')
    style.innerText = '* { color: red; }'
    var img = document.createElement('img')
    img.height = 300; 
    img.width = 300;
    img.src = 'https://www.baidu.com/img/PCtm_d9c8750bed0b3c7d089fa7d55720d6cf.png'
    var div = document.createElement('div')
    div.innerText = "这是百度网站"
    div.appendChild(style)
    div.appendChild(img)
    printElement(div)

现在我们就是想打印什么打印什么

demo 3 将 body 重置为目标 dom 元素, 打印后还原 body

示例网站: baidu.com

第一步: 调试控制台 - element 页签, 选中某一个元素

arduino 复制代码
// $0 是 chrome 调试工具选中的元素
// 没有使用过的话, 可以在 Element 页签点中任意一个 HTML 标签
// 然后控制台打印看看 $0 是啥

第二步: 调试控制台运行如下代码

方案特别简单, 就不解释源码了

ini 复制代码
    // $0 是 chrome 调试工具选中的元素
    let storeNodeHtml = document.body.innerHTML 
    let printNode = $0.cloneNode(true) 
    document.body.innerHTML = '' 
    document.body.innerHTML = printNode.innerHTML 
    window.print() 
    document.body.innerHTML = storeNodeHtml

打印整个网页还好, 打印局部可能会丢失样式;毕竟把某个元素直接丢进 <body> 元素, 丢失了层级结构, css 选择器将会失效; 自己试试就知道了;

demo 4 三方库 jsPDF

示例网站: google.com

第一步: 运行代码加载三方库

  • jsPDF 内部依赖 html2canvas
  • 百度网站对第三方库的加载做了限制, 这个 demo 不能在百度里运行 (有一些网站做了内容安全策略, 如 github 等, 所以这个 demo 的运行还挑网站 --- juejin 可以运行这个 demo)
  • 由于 jspdf 方案必须依赖字体库, 本 demo 不考虑解决这个问题, 所以挑选了一个几乎全英文的网站来进行演示
  • jspdf 方案, 必然要面对表格样式变形, 图片变形等问题, 比较难解决;
ini 复制代码
    // 控制台执行第一段
    var jspdfScript = document.createElement('script') 
    jspdfScript.src="https://unpkg.com/jspdf@latest/dist/jspdf.umd.min.js"; 
    document.body.append(jspdfScript)
    var html2canvasScript = document.createElement('script')
    html2canvasScript.src="https://html2canvas.hertzen.com/dist/html2canvas.min.js"; 
    document.body.append(html2canvasScript)

第二步: 调试控制台 - element 页签, 选中某一个元素, demo 直接选取 <body>

第三步: 调试控制台 - 运行如下代码

javascript 复制代码
    // 等 js 加载完成, 控制台执行第二段
    // $0 是调试工具选中的元素, 没有使用过的话, 可以直接控制台打印看看 $0 是啥
    window.jsPDF = jspdf.jsPDF
    var doc = new jsPDF()
    doc.html($0, {
      callback: function(doc) {
       doc.save('sample-document.pdf');
      },
      x: 1,
      y: 1,
      width: 170,
      windowWidth: 1650
    });

图片变形, 中文乱码....懒得解决

demo 5 jsPDF + html2canvas

示例网站: google.com

第一步: 运行代码加载三方库

  • jsPDF 内部依赖 html2canvas
  • 百度网站对第三方库的加载做了限制, 这个 demo 不能在百度里运行
ini 复制代码
// 控制台执行第一段
var jspdfScript = document.createElement('script') 
jspdfScript.src="https://unpkg.com/jspdf@latest/dist/jspdf.umd.min.js"; 
document.body.append(jspdfScript)
var html2canvasScript = document.createElement('script')
html2canvasScript.src="https://html2canvas.hertzen.com/dist/html2canvas.min.js"; 
document.body.append(html2canvasScript)

第二步: 调试控制台 - element 页签, 选中某一个元素, demo 直接选取 <body>

第三步: 调试控制台 - 运行如下代码

arduino 复制代码
    // 等 js 加载完成, 控制台执行第二段
    // $0 是调试工具选中的元素, 没有使用过的话, 可以直接控制台打印看看 $0 是啥
    window.jsPDF = jspdf.jsPDF
    html2canvas($0).then(function(canvas) {
        var max = { height:300 - 40 * 2, width: 210 - 15 * 2 }
        var doc = new jsPDF("p", "mm", 'a2')
        var height=canvas.height
        var width=canvas.width
        var ratio=canvas.height / canvas.width;
        if(height > max.height){// 先调整高
            height = max.height;
            width = height * ( 1 / ratio);
        }
        if (width > max.width) {// 再调整宽
            width = max.width
            height = width * ratio
        }
        // 最后宽高都是合适的
        doc.addImage(canvas, 'PNG', 15, 40, width, height)
        doc.save('sample-document.pdf')
    });

效果明显比 demo 4 要好, 并且没有乱码的问题,但这是一整张图片, 丢失了 pdf 内容可搜索, 可复制的特性;

我们的场景分析

将自定义长列表导出, 不需要操作按钮, 表格所有选项默认展开, 去掉装饰性元素.

方案概要 css 媒体查询 iframe 打开 DOM 元素 将 body 重置为目标 dom 元素, 打印后还原 body 三方库 jsPDF 三方库 jsPDF + 三方库 html2canvas
结论 不采纳 作为华为云官网微服务, 大量框架相关的元素和外部样式 采纳 需要重新编写一套模板和 css, 成本略略高, 但是完全可控; 备选 不需要重新编写全套样式, 在 angular 样式模块的背景下也不会丢失全局的样式; 会丢失所有 DOM 元素的监听事件...要全局重新渲染一遍; 不采纳 字体库 20M, 前端加载用户体验非常差 不采纳 丢失了 pdf 文本属性, 不可搜索, 不可复制粘贴, 不满足业务要求
相关推荐
ShenJLLL3 小时前
vue部分知识点.
前端·javascript·vue.js·前端框架
恋猫de小郭3 小时前
你是不是觉得 R8 很讨厌,但 Android 为什么选择 R8 ?也许你对 R8 还不够了解
android·前端·flutter
PineappleCoder4 小时前
告别“幻影坦克”:手把手教你丝滑规避布局抖动,让页面渲染快如闪电!
前端·性能优化
武帝为此4 小时前
【Shell变量替换与测试】
前端·chrome
CappuccinoRose5 小时前
CSS 语法学习文档(十九)
前端·css·属性·flex·grid·学习资源·格式化上下文
雷电法拉珑5 小时前
财务数据批量采集
linux·前端·python
We་ct6 小时前
LeetCode 105. 从前序与中序遍历序列构造二叉树:题解与思路解析
前端·算法·leetcode·链表·typescript
前端 贾公子6 小时前
深入理解 Vue3 的 v-model 及自定义指令的实现原理(下)
前端·html
Roc.Chang6 小时前
Vite 启动报错:listen EACCES: permission denied 0.0.0.0:80 解决方案
linux·前端·vue·vite