將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並不是最優解

 

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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM