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 文本属性, 不可搜索, 不可复制粘贴, 不满足业务要求
相关推荐
m0_748236115 分钟前
Calcite Web 项目常见问题解决方案
开发语言·前端·rust
Watermelo61717 分钟前
详解js柯里化原理及用法,探究柯里化在Redux Selector 的场景模拟、构建复杂的数据流管道、优化深度嵌套函数中的精妙应用
开发语言·前端·javascript·算法·数据挖掘·数据分析·ecmascript
m0_7482489419 分钟前
HTML5系列(11)-- Web 无障碍开发指南
前端·html·html5
m0_7482356131 分钟前
从零开始学前端之HTML(三)
前端·html
一个处女座的程序猿O(∩_∩)O2 小时前
小型 Vue 项目,该不该用 Pinia 、Vuex呢?
前端·javascript·vue.js
hackeroink5 小时前
【2024版】最新推荐好用的XSS漏洞扫描利用工具_xss扫描工具
前端·xss
迷雾漫步者7 小时前
Flutter组件————FloatingActionButton
前端·flutter·dart
向前看-7 小时前
验证码机制
前端·后端
燃先生._.8 小时前
Day-03 Vue(生命周期、生命周期钩子八个函数、工程化开发和脚手架、组件化开发、根组件、局部注册和全局注册的步骤)
前端·javascript·vue.js