将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并不是最优解

 

  可能写的比较乱,可能属于本身知道咋回事可是说不出来那种……        

相关文章
相关标签/搜索