將HTML頁面自動儲存為PDF檔案並上傳的兩種方式(一)-前端(react)方式
一、業務場景
公司的樣本檢測報告以React頁面的形式生成,已調整為A4大小的樣式並已實現分頁,業務上需要將這個網頁生成PDF檔案,並上傳到伺服器,後續會將這個檔案傳送給客戶(這裡不考慮)。
二、原來的實現形式
瀏覽器原生方法:window.print()可以將網頁儲存為PDF檔案,由於檢測報告的網頁已經調整為A4的樣式,所以儲存下來後即是一個標準的PDF文件,然後將儲存下來的PDF檔案上傳到伺服器,即可實現需求。
三、存在的問題
呼叫window.print()方法後需要手動儲存PDF到本地,然後手動上傳到伺服器。所以本文的目的是點選上傳PDF後自動將網頁生成PDF,然後自動上傳到伺服器,省略操作者手動儲存、手動上傳這兩個步驟
四、解決方法
根據“自動”這個需求,找到了兩種實現方式:
- 純前端方式,前端生成pdf後通過介面上傳到伺服器
- 後端(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並不是最優解。
可能寫的比較亂,可能屬於自己知道咋回事但是說不出來那種……