1. 程式人生 > >將HTML頁面自動儲存為PDF檔案並上傳的兩種方式(一)-前端(react)方式

將HTML頁面自動儲存為PDF檔案並上傳的兩種方式(一)-前端(react)方式

一、業務場景

  公司的樣本檢測報告以React頁面的形式生成,已調整為A4大小的樣式並已實現分頁,業務上需要將這個網頁生成PDF檔案,並上傳到伺服器,後續會將這個檔案傳送給客戶(這裡不考慮)。

二、原來的實現形式

  瀏覽器原生方法:window.print()可以將網頁儲存為PDF檔案,由於檢測報告的網頁已經調整為A4的樣式,所以儲存下來後即是一個標準的PDF文件,然後將儲存下來的PDF檔案上傳到伺服器,即可實現需求。

三、存在的問題

  呼叫window.print()方法後需要手動儲存PDF到本地,然後手動上傳到伺服器。所以本文的目的是點選上傳PDF後自動將網頁生成PDF,然後自動上傳到伺服器,省略操作者手動儲存、手動上傳這兩個步驟

四、解決方法

  根據“自動”這個需求,找到了兩種實現方式:

  1. 純前端方式,前端生成pdf後通過介面上傳到伺服器
  2. 後端(node)方式,通過另起一個node服務來生成pdf並上傳(推薦,以後介紹

四、純前端方法

  前端採用了React框架。另需要html2canvas,jspdf兩個庫。

  1、場景1-上傳一個尚未開啟的React頁面,這種情況下需要將需要上傳的頁面通過iframe以visiblity:hidden的形式開啟或者被遮擋在看不到的地方,不可以display:none,因為這樣獲取到的DOM元素樣式不正確,html2canvas會表現不正常。

  由於流程較多,直接見程式碼吧,說明見註釋:

// 生成或者獲取報告頁面的外部容器
const getIframeContainer = () => {
  const ic = document.getElementById("iframeContainer");
  if (!ic) {
    const iframeContainer = document.createElement("div");
    iframeContainer.id = "iframeContainer";
    iframeContainer.style.visibility = "hidden";
    document.body.appendChild(iframeContainer);
    
return iframeContainer; } return ic; }; class SendModal extends React.Component { // ... // 點選開始上傳 handleUpload = () => { // 獲取iframe容器和這個報告的ID const iframeContainer = getIframeContainer(); const iframeId = `iframe_${this.state.id}`; // iframe的load事件回撥,執行該回調後開始執行this.createAndUpload() const onloadCallback = () => { this.createAndUpload(iframeId).then( // resolve和reject後移除報告iframe () => { ReactDOM.unmountComponentAtNode(iframeContainer); }, errMsg => { ReactDOM.unmountComponentAtNode(iframeContainer); console.error(errMsg); } ); }; // 開始渲染報告的iframe ReactDOM.render( <ReportIframe id={iframeId} src={reportURL} onLoad={onloadCallback} key={iframeId} />, iframeContainer ); }; createAndUpload = iframeId => { return new Promise((resolve, reject) => { // 從iframe中獲取需要儲存為PDF的DOM元素 let pages = Array.from( document .getElementById(iframeId) .contentDocument.querySelectorAll(".pdfpage") ); console.log(pages); const pagesLen = pages.length; if (!pagesLen) { reject("開啟報告失敗!"); } // 初始化一個pdf待用 const doc = new jsPDF("p", "mm", "a4"); const imgArr = []; console.log("成功抓取pages"); // 將每個元素作為一個頁面處理 pages.forEach((page, idx) => { console.log(`正在繪製canvas[${idx}]`); html2canvas(page, { scale: 2, logging: false, useCORS: true, imageTimeout: 60000 }).then(canvas => { // canvas儲存為圖片 let imgData = canvas.toDataURL("image/jpeg", 1.0); imgArr.push({ index: idx, value: imgData }); if (imgArr.length === pagesLen) { console.log("canvas繪製完成,正在生成pdf"); // 通過idx保證頁面順序 let sortedArr = imgArr.sort((a, b) => a.index - b.index); sortedArr = sortedArr.map(item => item.value); sortedArr.forEach((img, idx) => { // 將圖片放入pdf檔案中 if (idx > 0) { doc.addPage(); } doc.addImage(img, "JPEG", 0, 0, 210, 297); if (idx + 1 === pagesLen) { // 全部放入pdf檔案後,儲存並上傳 const pdf = doc.output("blob"); console.log("成功生成pdf,正在上傳"); const formData = new FormData(); formData.append("file", pdf); fetch(`uploadURL`, { method: "post", body: formData }) .then(response => response.json()) .then(resp => { if (resp.Status === 0) { console.log("上傳成功"); resolve("success"); } else { console.log("上傳失敗"); reject("上傳報告失敗!"); } }); } }); } }); }); }); }; // ... } class ReportIframe extends React.Component { // React通過js渲染頁面,所以iframe觸發onload後可能頁面是一個空白頁面,所以通過getPages方法確保React渲染完成後出發onLoad回撥 getPages = (e, times = 1) => { const pages = Array.from( this.iframe.contentDocument.querySelectorAll(".pdfpage") ); if (pages.length || times >= 5) { this.props.onLoad(); this.iframe.removeEventListener("load", this.getPages); } else { setTimeout(() => { times++; this.getPages(e, times); }, 1000); } }; componentDidMount() { this.iframe.addEventListener("load", this.getPages, false); } render() { return ( <iframe id={this.props.id} src={this.props.src} ref={node => (this.iframe = node)} /> ); } }

  2、場景2-在已開啟頁面中生成pdf並上傳,程式碼同上,直接執行createAndUpload即可,不考慮iframe的相關處理。

五、效果演示

  首先在報告列表頁點擊發送按鈕,將進入待發送頁面:

  

 

   ↑點選確認傳送將會以iframe的形式自動開啟頁面並儲存為pdf上傳到伺服器然後傳送到客戶。

  

  ↑生成的iframe元素

  

  ↑上傳流程    

六、遇到的坑及說明

  1、生成的pdf模糊

  html2canvas設定scale:2可解決,即使用2倍圖保證清晰度。

  2、頁面中每頁的順序已排好,但是生成pdf後亂了

  由於canvas生成圖片這個過程是非同步的,所以我沒有直接將生成的圖片插入pdf中,而是通過idx排序後統一插入pdf。

  3、圖片跨域

  公司使用的阿里雲OSS,所以將圖片設定了Access-Control-Allow-Origin:*即可解決,如果是外部圖片,需要使用代理,具體使用見html2canvas相關文件。

  4、頁面中有虛線,但是html2canvas生成的是實線

  見我之前的文章

  5、新建iframe後getPages作用是什麼

  React通過js渲染頁面,所以iframe觸發onload後可能頁面是一個空白頁面,所以通過getPages方法確保React渲染完成後出發onLoad回撥

七、前端生成PDF總結

  前端生成pdf並上傳的流程:獲取將要作為PDF頁面的DOM元素 -> 將DOM元素生成canvas -> 將canvas轉為圖片 -> 將圖片插入pdf中 -> 將pdf上傳

  由於是通過轉成圖片生成的PDF,即使是2倍圖,清晰度依然不如原生PDF,且無法選擇文字,所以這種方式生成PDF並不是最優解

 

  可能寫的比較亂,可能屬於自己知道咋回事但是說不出來那種……