目錄
需求
一、方案調研
二、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 = falsethrow 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 = urla.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?什么是點擊劫持?如何防御