一, 业务场景
公司的样本检测报告以 React 页面的形式生成, 已调整为 A4 大小的样式并已实现分页, 业务上需要将这个网页生成 PDF 文件, 并上传到服务器, 后续会将这个文件发送给客户(这里不考虑).
二, 原来的实现形式
浏览器原生方法: Windows.print()可以将网页保存为 PDF 文件, 由于检测报告的网页已经调整为 A4 的样式, 所以保存下来后即是一个标准的 PDF 文档, 然后将保存下来的 PDF 文件上传到服务器, 即可实现需求.
三, 存在的问题
调用 Windows.print()方法后需要手动保存 PDF 到本地, 然后手动上传到服务器. 所以本文的目的是点击上传 PDF 后自动将网页生成 PDF, 然后自动上传到服务器, 省略操作者手动保存, 手动上传这两个步骤.
四, 解决方法
根据 "自动" 这个需求, 找到了两种实现方式:
纯前端方式, 前端生成 PDF 后通过接口上传到服务器
后端 (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 并不是最优解.
可能写的比较乱, 可能属于自己知道咋回事但是说不出来那种......
来源: https://www.cnblogs.com/zczhangcui/p/9805487.html