一、业务场景

  公司的样本检测报告以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并不是最优解。

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

将HTML页面自动保存为PDF文件并上传的两种方式(一)-前端(react)方式的更多相关文章

  1. curl文件上传有两种方式,一种是post_fileds,一种是infile

    curl文件上传有两种方式,一种是POSTFIELDS,一种是INFILE,POSTFIELDS传递@实际地址,INFILE传递文件流句柄! );curl_setopt($ch, CURLOPT_PO ...

  2. 利用Selenium实现图片文件上传的两种方式介绍

    在实现UI自动化测试过程中,有一类需求是实现图片上传,这种需求根据开发的实现方式,UI的实现方式也会不同. 一.直接利用Selenium实现 这种方式是最简单的一种实现方式,但是依赖于开发的实现. 当 ...

  3. SpringBoot从入门到精通十一(SpringBoot文件上传的两种方法)

    前言 在企业级项目开发过程中,上传文件是最常用到的功能.SpringBoot集成了SpringMVC,当然上传文件的方式跟SpringMVC没有什么出入. 本章目标 使用SpringBoot项目完成单 ...

  4. C#实现Web文件上传的两种方法

    1. C#实现Web文件的上传 在Web编程中,我们常需要把一些本地文件上传到Web服务器上,上传后,用户可以通过浏览器方便地浏览这些文件,应用十分广泛. 那么使用C#如何实现文件上传的功能呢?下面笔 ...

  5. linux 下文件上传的两种工具(XFTP5和Putty之pscp)方式

    一.使用XFTP(,需要先在LINUX上安装启用FTP服务) 然后,在WINDOWS上启动XFPT6客户端,将下载的文件上传至LINUX 指定目录: 二.使用PUTTY软件安装目录下的PSCP命令 1 ...

  6. ASP.NET Core 1.0中实现文件上传的两种方式(提交表单和采用AJAX)

    Bipin Joshi (http://www.binaryintellect.net/articles/f1cee257-378a-42c1-9f2f-075a3aed1d98.aspx) Uplo ...

  7. Html代码保存为Pdf文件

    前段时间Insus.NET有实现了<上传Text文档并转换为PDF>http://www.cnblogs.com/insus/p/4313092.html 和<截取视图某一段另存为部 ...

  8. 使用HttpClient实现文件的上传下载

    1 HTTP HTTP 协议可能是现在 Internet 上使用得最多.最重要的协议了,越来越多的 Java 应用程序需要直接通过 HTTP 协议来访问网络资源. 虽然在 JDK 的 java.net ...

  9. web.config文件中配置数据库连接的两种方式

    web.config文件中配置数据库连接的两种方式 标签: 数据库webconfig 2015-04-28 18:18 31590人阅读 评论(1)收藏举报    分类: 数据库(74)  在网站开发 ...

随机推荐

  1. git常用命令总结——你一定会用到的几个命令

    git入门看廖雪峰大神的教程即可,通俗易懂:      https://www.liaoxuefeng.com/wiki/0013739516305929606dd18361248578c67b806 ...

  2. 在Java里>、>>、>>>的含义

    在java中: >表示大于,如:if(a>b)...结果是boolean类型 >>表示右移,如:int i=15; i>>2的结果是3,移出的部分将被抛弃. 转为二 ...

  3. 1 分钟教会你用 Spring Boot 发邮件

    Spring Boot 提供了一个发送邮件的简单抽象,使用的是下面这个接口. org.springframework.mail.javamail.JavaMailSender Spring Boot ...

  4. iReport官方文档(英文版本)+ iReport中文教程

    背景介绍 最近学习iReport,但是网上找到的博客里都是断断续续,几乎没有很详尽的资料文档,要么下载收费,要么不全.很是困扰. 在此,我提供给大家两个官网文档: JasperReports Libr ...

  5. Linux 下 Shell 的自动交互

    在编写脚本的时候经常会遇到这种情况,某些程序的命令执行的之后可能会要求用户进行输入,这个时候就需要一些特殊写法来应对这种问题了.这里参考 这篇文章提到可以使用 delimiter 分界符来解决. 也就 ...

  6. 微信小程序入门(二)

    5.安装微信开发者工具 小程序入口文档 点"小程序开发"-->>"工具-->>再点左边的"下载",进行开发者工具的下载 6.小 ...

  7. 移动设备(手机)的唯一ID有哪些

    移动设备(手机)的唯一ID有哪些 在移动广告领域,设备的ID 是用来追踪一个人的最重要的标识. 对于APP自身产品而言,使用设备唯一ID可以追踪到用户从下载到激活.注册.使用.流失.回归的全流程数据, ...

  8. Mybatis Insert、update、delete流程

    上文mybatis源码简书我们讲到sqlsession中通过executor来执行sql,我们接着往下看 update方法点进去,我们进到baseexecutor 这里我们看到 clearLocalC ...

  9. 项目ITP(四) javaweb http json 交互 in action (服务端 spring 手机端 提供各种工具类)勿喷!

    前言 系列文章:[传送门] 洗了个澡,准备写篇博客.然后看书了.时间 3 7 分.我慢慢规律生活,向目标靠近.  很喜欢珍惜时间像叮当猫一样 正文 慢慢地,二维码实现签到将要落幕了.下篇文章出二维码实 ...

  10. nginx中root和alias的区别

    nginx中root和alias的区别