项目中想要将侧边弹窗里的内容导出成 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 文本属性, 不可搜索, 不可复制粘贴, 不满足业务要求 |