公司的樣本檢測報告以React頁面的形式生成,已調整爲A4大小的樣式並已實現分頁,業務上須要將這個網頁生成PDF文件,並上傳到服務器,後續會將這個文件發送給客戶(這裏不考慮)。html
瀏覽器原生方法:window.print()能夠將網頁保存爲PDF文件,因爲檢測報告的網頁已經調整爲A4的樣式,因此保存下來後便是一個標準的PDF文檔,而後將保存下來的PDF文件上傳到服務器,便可實現需求。前端
調用window.print()方法後須要手動保存PDF到本地,而後手動上傳到服務器。因此本文的目的是點擊上傳PDF後自動將網頁生成PDF,而後自動上傳到服務器,省略操做者手動保存、手動上傳這兩個步驟。node
根據「自動」這個需求,找到了兩種實現方式:json
前端採用了React框架。另須要html2canvas,jspdf兩個庫。canvas
因爲流程較多,直接見代碼吧,說明見註釋:後端
// 生成或者獲取報告頁面的外部容器 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)} /> ); } }
首先在報告列表頁點擊發送按鈕,將進入待發送頁面:跨域
↑點擊確認發送將會以iframe的形式自動打開頁面並保存爲pdf上傳到服務器而後發送到客戶。瀏覽器
↑生成的iframe元素服務器
↑上傳流程 app
html2canvas設置scale:2可解決,即便用2倍圖保證清晰度。
因爲canvas生成圖片這個過程是異步的,因此我沒有直接將生成的圖片插入pdf中,而是經過idx排序後統一插入pdf。
公司使用的阿里雲OSS,因此將圖片設置了Access-Control-Allow-Origin:*便可解決,若是是外部圖片,須要使用代理,具體使用見html2canvas相關文檔。
見我以前的文章
React經過js渲染頁面,因此iframe觸發onload後可能頁面是一個空白頁面,因此經過getPages方法確保React渲染完成後出發onLoad回調
前端生成pdf並上傳的流程:獲取將要做爲PDF頁面的DOM元素 -> 將DOM元素生成canvas -> 將canvas轉爲圖片 -> 將圖片插入pdf中 -> 將pdf上傳
因爲是經過轉成圖片生成的PDF,即便是2倍圖,清晰度依然不如原生PDF,且沒法選擇文字,因此這種方式生成PDF並不是最優解。
可能寫的比較亂,可能屬於本身知道咋回事可是說不出來那種……