html轉PDF檔案,完美解決方案——jsPDF,htmltocanvas,pdfmake,wkhtmltopdf,TuesPechkin,snappy

沈鑫Real發表於2018-05-26

-----------------------------------------2019年3月13日更新------------------------------------------------


這篇文章寫後有很多人私聊問我如何解決內容切分的問題,還有如何在文件中新增圖表等問題。這裡我對這些問題做一個答覆,希望能夠幫到大家。

在我們使用wkhtmltopdf工具將html頁面轉換成pdf的時候,如果不想讓內容被切分,則需要給工具一個明確的指示,這個指示就是 css。

防止內容被切分

在每個章節的標題或者其他地方我們往往不希望標題被切成兩半,分別出現在兩個頁面當中。因此,我們需要新增如下樣式:

.title {
    page-break-before: always;
    page-break-after: always;
    page-break-inside: avoid;
}
複製程式碼

樣式的含義已經一目瞭然了,當wkhtmltopdf工具在渲染到標有.title樣式的元素時如果剩餘空間不足以放置該元素,則會將元素另起一頁。

同樣,在一般的公司文件中會出現大量的表格。如果希望放置表格被切分也是同樣的處理方式:

table tr {
    word-break: break-all;
    page-break-before: always;
    page-break-after: always;
    page-break-inside: avoid;
}
複製程式碼

有一個技巧,如果希望表格分佈在兩個頁面時,第二頁表格依然保有表頭,則需要在編寫table的時候將元素進行結構化,即分為:<thead>,<tbody>,<tfoot>

在文件中新增圖表

我生成圖表的方式是使用了echarts庫。很多人覺得圖表的資料是通過http請求的來的,然後基於資料生成圖表,所以一遇到圖表就不知道該怎麼辦了。其實圖表和普通的html元素並沒有什麼不同。遇到這種問題我採取的辦法就是先利用獲取的資料將頁面渲染好,然後用wkhtmltopdf工具將渲染好的頁面生成pdf。

具體方法有兩種:

  • 方法一:前端在瀏覽器中將整個頁面渲染好,然後將整個頁面通過http請求傳送至後臺,後臺在接收到html資料後,利用接收到的資料生成一個html檔案,再用wkhtmltopdf工具去處理生成的html檔案即可。因為此時的html檔案只是一個靜態頁面,裡面的所有資料都是固定的,不會再存在非同步的問題。
  • 方法二:另一個做法就是利用服務端渲染。有過傳統web專案開發的人都知道,以前的html頁面都是通過後臺渲染然後推送至瀏覽器進行展示的。使用的工具有freemarker, themeleaf等。前端開發人員將整個靜態html開發好交給後臺開發人員,後臺開發人員通過themeleaf等工具將後臺資料推送至頁面並渲染。然後將生成的html頁面利用wkhtmltopdf生成pdf檔案。這樣的好處就是可以後臺進行pdf檔案的生成。

第一種方法的弊端就是每次必須通過瀏覽器將頁面開啟,而第二種方法則不需要。

將特定內容單獨另起一頁

有時候我們的文件可能分為幾個部分,每個部份需要從新的一頁開始展示。但是通過以上方法並不能解決這個問題。所以我們需要對每個需要單獨展示的部分進行特殊處理。其實處理的方式與防止內容被切分是一樣的。如下:

當你需要在當前部分之前進行分頁時:

.page-break-before {
    page-break-before: always;
}
複製程式碼

當你需要在當前部分之後進行分頁時:

.page-break-after {
    page-break-after: always;
}
複製程式碼

總結

到這裡應該能夠滿足大多數的開發需求了,希望能夠幫到大家。如果本文對你有幫助請為我點個贊,你的支援是我繼續分享的最大動力。

---------------------------------------------- 分割線 ------------------------------------------------


最近新換了一個公司,一入職就給了我一個報表下載的專案,從零開始做起。因為之前也做過匯出Excel和PDF的相關工作,所以一開始並不覺得有什麼困難。直到看到UI設計出的報表之後我的內心是崩潰的。整個報表很長,有各種樣式,各種Table。。。這跟用Excel匯出一系列資料,或者生成一個簡單PDF是很不一樣的。因為這兩種情況都可以通過後臺實現,而且也不復雜。

關於如何通過後臺生成一個Excel與PDF這裡就不做介紹了,java裡都有現成的外掛。

先來介紹下我的開發環境:

  • Vue.js
  • Vuex
  • vue-router
  • vue-cli(所以專案構建使用的是webpack)

下面來說說這一路走過來我摔了幾次鍵盤。

第一次摔鍵盤:

拿到任務第一時間開始到網上找各種方案,參考下各位大牛有沒有成熟的解決方案,畢竟這也算是一個合理且應用場景比較廣泛的需求。我堅信網上一定會有外掛提供類似功能。

過程很順利,我成功找到了jsPDF,jsPDF是一款能夠在前端生成PDF並下載的外掛,感覺很牛逼。通過jsPDF與htmltocanvas配合使用就能實現將html頁面轉換成PDF檔案並下載。原理就是通過htmltocanvas給html頁面拍個照,然後將頁面儲存在canvas中,再通過jsPDf將canvas貼到PDF檔案中。所以,本質上生成的生成的PDF其實裡面就是一張圖片。

到這裡,一切都很完美。。。但是!只能生成單頁PDF!!!這麼坑?我是不信的,後來找個一個方案:根據a4紙的高度將生成的canvas圖片截成一塊一塊,然後再分別貼入PDF的不同頁面中。這樣就能夠給生成多頁面PDF了。注意:例子裡面的呼叫方法已經過時了,可以參考html2canvas官網的例子。

但是!生成的PDF會被截斷!不只是圖片會被截斷,甚至文字也會被截斷。這簡直讓人無法忍受,於是果斷摔鍵盤,換!方!案!

這裡給出幾個jsPDF的官方網站,如果鍵盤多的土豪可以研究一下:

各位老闆可別打我,你可能發現網站打不開,於是很心急的買了個VPN,結果發現還是打不開。這時候不要懷疑,沒錯,後面兩個官方的網址就是打!不!開!所以根本沒有文件可以參考,甚至有什麼API都不知道。。。有前同事很自(chou)信(pi)的說:你可以去看原始檔啊。我只能說:嗯嗯,你說的都對。

第二次摔鍵盤:

第一種方案失敗後萬念俱灰,因為網上解決生成PDF檔案的方案基本上都是使用jsPDF+htmltocanvas。可是這個方案如此不靠譜,不知道還有沒有其他更好的方案,心裡有點沒底。

經過好長時間的查詢,終於在一個提問的回答中看到了一個關鍵字:wkhtmltopdf,於是趕緊搜尋。於是找到了一篇介紹wkhtmltopdf與jsPDF優缺點的文章,簡直是找到了知音有木有,遇到的問題一樣一樣的。

找到方案之後立馬開始嘗試,wkhtmltopdf的執行語法是這樣的:

wkhtmltopdf htmlPath ouputPath
複製程式碼

其中htmlPath是檔案路徑,可以是網路地址,也可以是本地檔案地址。outputPath是匯出的PDF檔案的存放路徑。

在網上找了幾個網址,比如CSDN什麼的試了一下,還真的可以!生成的PDF檔案能夠自動分頁,唯一的問題就是內容還是會被切分。不過在這之前我已經找到避免分頁時,內容被切分的方案了,所以暫時沒有理會切分問題,想著回頭再解決。具體解決方案我會在接下來的內容裡介紹。

有了初步成果之後我開始用我自己的頁面做嘗試,看看能否順利轉成PDF檔案。

。。。。。。。。。耐心等待中。。。。。。。。。

喜聞樂見,失敗了。。。檔案是生成了,但是裡面的內容時一片空白。。。

於是又開始了無盡的google之旅。翻了無數部落格之後找了一篇關於wkhtmltopdf匯出檔案空白的部落格,裡面給出了一些可能會導致文件空白的例子。我的空白問題並不是這些情況引起的,因此我沒有采用這個方案。如何在vue專案中解決空白問題我會單獨寫一篇部落格,這裡只介紹一些通用情況。在我的專案中引起空白頁的主要原因是就是,使用webpack打包的專案,index.html頁面在檢視的時候是不顯示具體頁面內容的,具體內容都包含在一個js檔案中,只有在訪問到具體某個頁面路由的時候相應的資源才會被調出來。但是wkthtmltopdf只能解析靜態資源,不會去執行js檔案。因此,index.html中包含什麼內容,匯出來的PDF檔案就包含哪些內容。

這裡介紹一個判斷當前頁面能否被wkhtmltopdf正常匯出的一個方法:

將當前頁面在瀏覽器中另存為,儲存到本地,如果本地檔案開啟後是有內容的,那麼wkhtmltopdf就能正常匯出。因為這個工具只會解析html與CSS,並不會去執行js檔案。所以在webpack這種專案中,所有資源都被打包成一個js檔案,wkhtmltopdf就無法正常匯出了。還有一個問題就是:如果頁面中的內容時在頁面開始渲染時才通過ajax請求從後臺獲取的,也是無法被渲染出來的,原因還是wkhtmltopdf不會去執行js檔案,所以在渲染的時候ajax請求是不會傳送的,更加不會被渲染到頁面上。

這個問題我的解決方案是:等待頁面正常渲染過後將整個頁面傳給後臺,後臺在接到頁面資料後在本地儲存為一個html檔案,然後使用wkhtmltopdf將本地html檔案轉換成PDF檔案。不過這樣做要注意:一定要在後臺提前準備好靜態資源,否則在生產環境下CSS樣式和圖片什麼的就無法渲染。

獲取頁面資訊的程式碼如下:

let htmlEle = document.querySelector('#downloadPaper');
複製程式碼

為index.html頁面的html跟標籤附一個id值:downloadPaper,然後通過ajax請求將整個頁面資訊傳遞給後臺。注意:傳送型別為post,引數型別為'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8'(具體型別還是要以後臺實際為準)。在傳參的時候記得取出htmlEle中的html元素:

{
    searchData: htmlEle.innerHTML
}
複製程式碼

其他也沒啥了,後臺在成功生成PDF檔案後就可以返回檔案路徑啦,然後前端呼叫下載介面,將檔案路徑傳遞回去就能下載檔案啦:

window.open(vm.apiHost + 'download?pdfFilePath=' + res.pdfFilePath)
複製程式碼

到此為止,已經可以成功下載PDF檔案啦,如果你在後臺準備了靜態檔案,生成的PDF檔案應該是包含樣式的。但是背景圖這種較大的圖片因為無法被壓縮成base64,所以也需要在靜態問價那種包含,並將路徑配置正確。

為了慶祝一下階段性成果,我還是要怒砸鍵盤!因為匯出的內容切割問題還沒解決。今天寫的有點累了,內容切割問題下次再補充吧,休息一會兒。

對了還可以使用Freemarker來生成PDF檔案,但是因為一開始把這個方案忘了(其實是不知道怎麼在Vue框架中使用),所以就沒有研究這種方案。

相關文章