目录
需求
一、方案调研
二、wkhtmltopdf使用
如何使用
文档简要说明
三、后端服务
四、前端服务
往期回顾
需求
最近在做报表类的统计项目,其中有很多指标需要汇总,网页内容有大量的echart图表,做成一个网页去浏览,同时需要转成PDF格式下载浏览,更重要的是pdf格式再打开后,需要自定义页眉、页脚,页码,支持文本的选中、复制、粘贴,同时左侧也要有正常的页签导航,点击哪里到哪里。
一、方案调研
经过调研主要有以下几种方式生成pdf,但是每个方案都有缺陷,跟我们的需求相差。
方案 | 优点 | 缺点 |
window.print() | 1、兼容性最好 2、可以将任意内容导出成 pdf 文档, 甚至是非改页面上的内容 | 1、调用方法时部分条件下导出pdf需要用户手动选择 2、生成的pdf不支持生成页签导航 3、页眉页脚不适合自定义 |
jspdf + html2canvas | 1、在jspdf上将生成效果不佳的部分可以转成图片,适用于对样式有要求的场景 2、将乱码部分转为了图片,解决了中文乱码问题 3、没有预览点击即可保存 | 1、如果内容包含echart图表或者其它图表,该内容需要转图片 5、pdf分页不好处理 6、不支持生成页签导航 |
wkhtmltopdf | 1、支持自定义页眉页脚页码 2、支持文本选中粘贴复制 3、支持将html的h标签自动生成pdf | 1、需要结合后端去实现生成接口返回给前端下载 2、 3、 |
前两种是纯前端去实现的方案,一是用浏览器打印功能实现,这种方案简单粗暴,但是需要手动触发,不支持自定义页眉页脚页码,浏览器也不支持生成页签导航。第二种把整个页面生成图片,完整还原了样式但是,跟我们的要求差太远。第三种是wkhtmltopdf,底层是C++去实现的,能够高效地将 HTML 内容转换为高质量的 PDF 文件。下面主要介绍下wkhtmltopdf使用。
二、wkhtmltopdf使用
官网入口:wkhtmltopdf
如何使用
- 下载预编译的二进制文件或从源代码构建
下载链接:wkhtmltopdf
以下是适配所有操作系统的包,我们根据自己的系统不同的下载包
以centeros7为例
1.首先我们下载我们需要的包
我的是x86_64的,下载完成后将包传到服务器
运行命令安装
rpm -Uvh wkhtmltox-0.12.6-1.centos7.x86_64.rpm
复制
报错!!!
原因是缺少依赖,我们来安装下依赖
yum install fontconfig libX11 libXext libXrender libjpeg libpng xorg-x11-fonts-Type1
复制
yum install -y xorg-x11-fonts-75dpi
复制
再次运行安装命令
查看版本
wkhtmltopdf --version
复制
大功告成! YYDS!
安装完成后我们来使用它
- 创建要转换为PDF或者图像的HTML文档
- 通过命令运行工具生成PDF
比如我要将Google网页保存为pdf,则可以直接运行命令
wkhtmltopdf http://google.com google.pdf
复制
文档简要说明
官方文档说明:https://wkhtmltopdf.org/usage/wkhtmltopdf.txt
强烈建议查看官方文档,以下(基于0.12.6的版本)
1. 基本命令
wkhtmltopdf [选项] <输入文件或URL> <输出PDF文件>
复制
示例:
wkhtmltopdf input.html output.pdf
复制
2.大纲(必要实现)
大纲就是PDF阅读器中,用于显示导航跳转的部分,不属于PDF文档中的一部分,主要是方便阅读器浏览导航使用。
Wkhtmltopdf 用 patched qt 支持PDF大纲(也称为书签),可以通过设置--outline
(默认选项)选项实现。
大纲是根据 <h?>
(h1–h6) 标签生成的,有关如何实现的详细说明,请参见目录部分。
如果 <h?>
标签在HTML文档中嵌套的层级非常深,那么大纲树的层级也会变得非常深。可以通过--outline-depth
选项来设置大纲的层级深度。
详细使用参考这篇文章哈哈哈
wkhtmltopdf 0.12.6 中文文档(精心整理)-CSDN博客
原理是:wkhtmltopdf将整个带css的html文档转为了pdf,因此想要 将我们前端画的好看的页面生成pdf,需要将html文档传给wkhtmltopdf。
三、后端服务
我们需要写一个后端服务,通过接口将前端绘制的漂亮页面整个以api的方式传给后端,后端将文档内容整理后,调用wkhtmltopdf的命令来生成pdf,然后返回文件流给前端提供下载。
npm为我们提供了调用wkhtmltopdf服务的插件
wkhtmltopdf - npm
以下是简单用法,以官方最新为准
var wkhtmltopdf = require('wkhtmltopdf'); // URL wkhtmltopdf('http://google.com/', { pageSize: 'letter' }) .pipe(fs.createWriteStream('out.pdf')); // HTML wkhtmltopdf('<h1>Test</h1><p>Hello world</p>') .pipe(res); // Stream input and output var stream = wkhtmltopdf(fs.createReadStream('file.html')); // output to a file directly wkhtmltopdf('http://apple.com/', { output: 'out.pdf' }); // Optional callback wkhtmltopdf('http://google.com/', { pageSize: 'letter' }, function (err, stream) { // do whatever with the stream }); // Repeatable options wkhtmltopdf('http://google.com/', { allow : ['path1', 'path2'], customHeader : [ ['name1', 'value1'], ['name2', 'value2'] ] }); // Ignore warning strings wkhtmltopdf('http://apple.com/', { output: 'out.pdf', ignore: ['QFont::setPixelSize: Pixel size <= 0 (0)'] }); // RegExp also acceptable wkhtmltopdf('http://apple.com/', { output: 'out.pdf', ignore: [/QFont::setPixelSize/] });
复制
以下是我写的一个简单的node server.js调用案列
const express = require('express'); const path = require('path'); const app = express(); const port = 3002; // 引入 cors 中间件 const cors = require('cors'); // 使用 cors 中间件 app.use(cors()); const fs = require('fs'); // 解析 JSON 请求体,设置最大限制为 50MB app.use(express.json({ limit: '50mb' })); // 解析 application/x-www-form-urlencoded 请求体,设置最大限制为 50MB app.use(express.urlencoded({ extended: true, limit: '50mb' })); // PDF生成高并发处理 function getPdfHeavyTask(html) { const wkhtmltopdf = require('wkhtmltopdf'); const options = { output: `./pdfs/demo.pdf`, pageSize: 'letter', orientation: 'portrait', marginTop: '1.8cm', marginBottom: '1.2cm', marginLeft: '1cm', marginRight: '1cm', encoding: 'UTF-8', dpi: 300, zoom: 1, title: 'pdf生成demo', enableSmartShrinking: true, javascriptDelay: 1000, noStopSlowScripts: true, headerHtml: './template/header.html', // 设置页眉模板 footerHtml: './template/footer.html' // 设置页脚模板 }; return new Promise((resolve) => { wkhtmltopdf(html, options, (err, stream) => { if (err) { resolve({ status: 500, data: err }); return; } resolve({ status: 200, data: stream }); }); }); } app.post('/generate-pdf', async (req, res) => { const { content, css } = req.body; let html = ` <!DOCTYPE html> <html lang="zh-CN"> <head> <meta charset="UTF-8"> <title>pdf生成demo</title> <style> body { font-family: "Microsoft YaHei", "SimSun", sans-serif; } ${css} </style> </head> <body> ${content} </body> </html> `; // 高并发生成异步任务处理 const { status, data } = await getPdfHeavyTask(html); // PDF生成失败 if (status === 500) { res.status(500).send(data); return; } // PDF生成成功读取 const filePath = path.resolve(__dirname, './pdfs/demo.pdf'); const fileStream = fs.createReadStream(filePath); const stat = fs.statSync(filePath); res.setHeader('Content-Length', stat.size); res.setHeader('Content-Type', 'application/pdf'); res.setHeader('Content-Disposition', 'attachment; filename=demo.pdf'); fileStream.pipe(res); }); app.listen(port, () => { console.log(`Server running at http://localhost:${port}`); });
复制
页眉页脚代码根据自己的需求添加即可
案例:header.html 自定义页码
<!DOCTYPE html> <html> <head> <script> function subst() { var vars = {}; var query_strings_from_url = document.location.search.substring(1).split('&'); for (var query_string in query_strings_from_url) { if (query_strings_from_url.hasOwnProperty(query_string)) { var temp_var = query_strings_from_url[query_string].split('=', 2); vars[temp_var[0]] = decodeURI(temp_var[1]); } } var css_selector_classes = ['page', 'frompage', 'topage', 'webpage', 'section', 'subsection', 'date', 'isodate', 'time', 'title', 'doctitle', 'sitepage', 'sitepages']; for (var css_class in css_selector_classes) { if (css_selector_classes.hasOwnProperty(css_class)) { var element = document.getElementsByClassName(css_selector_classes[css_class]); for (var j = 0; j < element.length; ++j) { element[j].textContent = vars[css_selector_classes[css_class]]; } } } } </script> </head> <body style="border:0; margin: 0;" onload="subst()"> <table style="border-bottom: 1px solid black; width: 100%"> <tr> <td class="section"></td> <td style="text-align:right"> Page <span class="page"></span> of <span class="topage"></span> </td> </tr> </table> </body> </html>
复制
四、前端服务
前端只需要将我们的html和css通过接口传给后端即可
try { const htmlContent = document.getElementById('report-content').outerHTML // 使用fetch API获取CSS文件 const response = await fetch('../../assets/core-report.css') const css = await response.text() this.http .post( '/generate-pdf', { content: htmlContent, // 网址或者HTML文档 css, }, undefined, { responseType: 'arraybuffer', observe: 'response', } ) .subscribe( (response: any) => { if (!response) { this.dloading = false throw new Error('生成 PDF 失败') } this.downloadProgress = 100 // 将 ArrayBuffer 转换为 Blob 对象 const blob = new Blob([response.body], { type: 'application/pdf' }) // 创建一个 URL 对象 const url = URL.createObjectURL(blob) // 下载 PDF 文件 const a = document.createElement('a') a.href = url a.download = `demo.pdf` document.body.appendChild(a) a.click() document.body.removeChild(a) URL.revokeObjectURL(url) }, (error) => { console.error('PDF生成失败:', error) } ) } catch (error) { console.error('PDF生成失败:', error) }
复制
我们通过脚本获取到html文档,通过fetch直接将文件内容获取,然后通过接口将两个参数传给后端,后端通过将两个内容组装成完整html,调用wkhtmltopdf,生成pdf,在通过文件流返回前端下载。这样生成的pdf,支持文本选中、复制、搜索,同时它会根据H标签识别页签导航内容,实现页签点击导航,YYDS!
注意点:
1:如果内容中存在canvas或者图片需要转base64传给后端,或者使用cdn链接
2:css3中的样式不支持,比如:阴影,以及flex布局不支持
3:内容被切分
在每个章节的标题或者其他地方我们往往不希望标题被切成两半,分别出现在两个页面当中。因此,我们需要添加如下样式:
.title { page-break-before: always; page-break-after: always; page-break-inside: avoid; }
复制
4: 表格切分
文档中会出现大量的表格。如果希望放置表格被切分也是同样的处理方式
table tr { word-break: break-all; page-break-before: always; page-break-after: always; page-break-inside: avoid; }
复制
欢迎在评论区交流。
如果文章对你有所帮助,❤️关注+点赞❤️鼓励一下!博主会持续更新。。。。
往期回顾
CSS多栏布局-两栏布局和三栏布局
border边框影响布局解决方案
css 设置字体渐变色和阴影
css 重置样式表(Normalize.css)
css实现元素居中的6种方法
Angular8升级至Angular13遇到的问题
前端vscode必备插件(强烈推荐)
Webpack性能优化
vite构建如何兼容低版本浏览器
前端性能优化9大策略(面试一网打尽)!
vue3.x使用prerender-spa-plugin预渲染达到SEO优化
vite构建打包性能优化
vue3.x使用prerender-spa-plugin预渲染达到SEO优化
ES6实用的技巧和方法有哪些?
css超出部分显示省略号
vue3使用i18n 实现国际化
vue3中使用prismjs或者highlight.js实现代码高亮
什么是 XSS 攻击?什么是 CSRF?什么是点击劫持?如何防御