將HTML頁面自動保存爲PDF文件並上傳的兩種方式(一)-前端(react)方式

1、業務場景

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

2、原來的實現形式

  瀏覽器原生方法:window.print()能夠將網頁保存爲PDF文件,因爲檢測報告的網頁已經調整爲A4的樣式,因此保存下來後便是一個標準的PDF文檔,而後將保存下來的PDF文件上傳到服務器,便可實現需求。前端

3、存在的問題

  調用window.print()方法後須要手動保存PDF到本地,而後手動上傳到服務器。因此本文的目的是點擊上傳PDF後自動將網頁生成PDF,而後自動上傳到服務器,省略操做者手動保存、手動上傳這兩個步驟node

4、解決方法

  根據「自動」這個需求,找到了兩種實現方式:json

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

4、純前端方法

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

  一、場景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-在已打開頁面中生成pdf並上傳,代碼同上,直接執行createAndUpload便可,不考慮iframe的相關處理。

5、效果演示

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

  

 

   ↑點擊確認發送將會以iframe的形式自動打開頁面並保存爲pdf上傳到服務器而後發送到客戶。瀏覽器

  

  ↑生成的iframe元素服務器

  

  ↑上傳流程    app

6、遇到的坑及說明

  一、生成的pdf模糊

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

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

  因爲canvas生成圖片這個過程是異步的,因此我沒有直接將生成的圖片插入pdf中,而是經過idx排序後統一插入pdf。

  三、圖片跨域

  公司使用的阿里雲OSS,因此將圖片設置了Access-Control-Allow-Origin:*便可解決,若是是外部圖片,須要使用代理,具體使用見html2canvas相關文檔。

  四、頁面中有虛線,可是html2canvas生成的是實線

  見我以前的文章

  五、新建iframe後getPages做用是什麼

  React經過js渲染頁面,因此iframe觸發onload後可能頁面是一個空白頁面,因此經過getPages方法確保React渲染完成後出發onLoad回調

7、前端生成PDF總結

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

  因爲是經過轉成圖片生成的PDF,即便是2倍圖,清晰度依然不如原生PDF,且沒法選擇文字,因此這種方式生成PDF並不是最優解

 

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

相關文章
相關標籤/搜索