React+html2canvas+jspdf+antd快速实现前端pdf预览及打印

简介: 文章的总结目标实际上就是一个前端pdf打印组件,由於能在往后的其他项目中得以快速上手,并能根据所在项目需要快速自定义扩展,因此組件非常简陋直白,文章是实践过程的记录产物,并不保证完全正确,仅作参考。

(1)依赖如下:

html2canvas配置参考:http://html2canvas.hertzen.com/configuration

jspdf配置参考: http://mozilla.github.io/pdf.js

"dependencies": {
"html2canvas": "1.4.1",
"jspdf": "2.5.1",
"ahooks": "^2.4.0",
"antd": "^4.5.4"  },

(2)主要流程如下:传进打印模板与模板填充参数  -> ReactDOM渲染模板到真实节点dom -> html2Canvas把节点dom生成图片 -> JsPDF把图片转成pdf -> pdf打印或下载,直接上代码,useHtml2Pdf.js如下:

importReact, { createRef, useState } from'react';
importReactDOMfrom'react-dom';
import { usePersistFn } from'ahooks';
import { Modal } from'antd';
import { FilePdfOutlined } from'@ant-design/icons';
importJsPDFfrom'jspdf';
importhtml2Canvasfrom'html2canvas';
// a4纸的尺寸(偏差 < 1pt)constA4_HEIGHT=840;
constA4_WIDTH=595;
constPdfPrint= () => {
const [loading, setloading] =useState(false);
constpdfRef=createRef();
consthandlePrint=usePersistFn(async ({ Template, isPrint, filename, options, callBack }) => {
setloading(true);
constprintDoc=document.createElement('div');
// 隐藏创建的domprintDoc.setAttribute('style', 'position:fixed;left:0;top:0;z-index: -999');
document.body.appendChild(printDoc);
// 通过React的render能力实现所需dom的渲染(包括css构建、变量填充)ReactDOM.render(<TemplatepdfRef={pdfRef} options={options} />, printDoc, async () => {consttargetDom=pdfRef.current;
// 克隆节点,默认为false,不复制方法属性,为true是全部复制。constcloneDom=targetDom.cloneNode(true);
// 设置克隆节点的css属性,因为之前的层级为0,我们只需要比被克隆的节点层级低即可。cloneDom.style.position='absolute';
cloneDom.style.top='0';
cloneDom.style.index='-999';
// cloneDom.style.height = height;cloneDom.style.display='block';
// 将克隆节点动态追加到body后面。printDoc.appendChild(cloneDom);
// 插件生成base64img图片。constcanvas=awaithtml2Canvas(cloneDom, {
useCORS: true,
y: 0, // 画布开始渲染的y坐标位置scale: window.devicePixelRatio*2,
      });
constcontentWidth=canvas.width;
constcontentHeight=canvas.height;
// 一页pdf显示html页面生成的canvas高度;constpageHeight= (contentWidth/A4_WIDTH) *A4_HEIGHT;
// html页面生成的canvas在pdf中图片的宽高constimgWidth=A4_WIDTH;
constimgHeight= (A4_WIDTH/contentWidth) *contentHeight;
constpageData=canvas.toDataURL('image/jpeg', 1.0);
constpdf=newJsPDF({
unit: 'pt',
format: 'a4',
orientation: 'p',
      });
// 未生成pdf的html页面高度letleftHeight=contentHeight;
// 页面偏移letposition=0;
// 有两个高度需要区分,一个是html页面的实际高度,和生成pdf的页面高度(A4_HEIGHT)// 当内容未超过pdf一页显示的范围,无需分页if (leftHeight<pageHeight) {
pdf.addImage(pageData, 'JPEG', 0, 0, imgWidth, imgHeight);
      } else {
while (leftHeight>0) {
pdf.addImage(pageData, 'JPEG', 0, position, imgWidth, imgHeight);
leftHeight-=pageHeight;
// 避免添加空白页position-=A4_HEIGHT;
if (leftHeight>0) {
pdf.addPage();
          }
        }
      }
// 打印if (isPrint) {
pdf.output('dataurlnewwindow', {
filename: filename??'foo.pdf',
        });
      }
// 下载if (!isPrint) pdf.save(filename??'bar.pdf');
// 删除利用完的dom部分document.body.removeChild(printDoc);
// 执行回调callBack&&callBack();
setloading(false);
    });
  });
// pdf预览constshowPdf= ({ isPrint, options, Template, callBack, filename }) => {
Modal.confirm({
width: 900,
title: '預覽',
icon: <FilePdfOutlined/>,
content: (
<divstyle={{ border: '1px solid #000' }}><Templateoptions={options} /></div>      ),
okText: `${isPrint?'打印' : '下載'}`,
cancelText: '取消',
onOk: () =>handlePrint({ isPrint, options, Template, callBack, filename }),
    });
  };
/*   *@Description:参数注解   *filename(pdf名稱)   *Template(模板)   *isPreviewPdf(是否预览)   *options(模板填充变量)   *callBack(回调函数)   */return {
print: ({ Template, isPreviewPdf, options, callBack, filename }) => {
isPreviewPdf?showPdf({ Template, options, callBack, filename, isPrint: true })
        : handlePrint({ Template, options, callBack, filename, isPrint: true });
    },
download: ({ Template, isPreviewPdf, options, callBack, filename }) => {
isPreviewPdf?showPdf({ Template, options, callBack, filename, isPrint: false })
        : handlePrint({ Template, options, callBack, filename, isPrint: false });
    },
loading,
pdfRef, // 模板引用  };
};
exportdefaultPdfPrint;

(3)为了使模板看起来更整洁,同时也为了区分复杂的业务处理逻辑,本次实践把字段映射与模板分离,字段映射文件map_conf.js如下:

// 对字段进行映射及逻辑处理exportconsttemplateMap= (options) => {
return {
zk1: options.nameAndCardNoZhHant??'--',
zk2: options.applyCodeZhHant??'--',
zk3: options.payTimeZhHant??'--',
zk4: options.activityNameZhHant??'--',
zk5: [
      {
total: options.total??'--',
discount: options.discount??'--',
amount: options.amount??'--',
payType: options.payTypeZhHant??'--',
      },
    ],
zk6: options.nowDateZhHant??'--',
col1: [
      {
title: '序号',
dataIndex: 'total',
width: '20%',
      },
      {
title: '金额',
dataIndex: 'discount',
width: '20%',
      },
      {
title: '统计',
dataIndex: 'amount',
width: '20%',
      },
      {
title: '方式',
dataIndex: 'payType',
width: '20%',
      },
    ],
  };
};

(4)对于模板的样式,目前发现使用pt作为长宽单位较为友好(注意:此非结论,而是快速实践过程中某个较优解,即真实dom与html2Canvas生成的图片误差极小),以下模板及样式仅作参考。

Template.jsx如下:

importReactfrom'react';
import { Table } from'antd';
import { templateMap } from'./map_conf';
import'./Template.less';
// 模板htmlconstTemplate= (props) => {
const { pdfRef, options } =props;
const { zk1, zk2, zk3, zk4, zk5, zk6, col1 } =templateMap(options);
return (
<divclassName="pdf-root"ref={pdfRef}><divclassName="one"><divclassName="title"><span>XX收據</span></div><divclassName="count"><divclassName="row"><spanclassName="label">已收到:</span><spanclassName="value  mocaofont">{zk1}</span></div></div><divclassName="count"><divclassName="item"><spanclassName="label">申请編號:</span><spanclassName="value">{zk2}</span></div><divclassName="item"><spanclassName="label">辦理日期:</span><spanclassName="value">{zk3}</span></div></div><divclassName="count"><divclassName="row"><spanclassName="label">如下项目:</span><spanclassName="value">{zk4}</span></div></div><divclassName="table"><TablerowKey={(_record, index) =>index.toString()}
columns={col1}
dataSource={zk5}
pagination={false}
size="small"bordered/></div></div><divclassName="two"><divclassName="count"><divclassName="row"><spanclassName="label">備注:</span><spanclassName="value">{zk6}</span></div></div></div></div>  );
};
exportdefaultTemplate;

Template.less如下:

.pdf-root {
width: 595pt;
margin: 0auto;
padding: 010pt;
font-size: 8pt;
color: #000;
font-family: 'Microsoft YaHei';
font-weight: 500;
.one {
height: 200pt;
padding-top: 20pt;
.count {
.item {
line-height: 14pt;
      }
.row {
line-height: 14pt;
      }
    }
  }
.two {
height: 210pt;
padding-top: 10pt;
  }
.title {
text-align: center;
line-height: 12pt;
font-size: 12pt;
font-weight: bold;
padding-bottom: 8pt;
  }
.count {
width: 100%;
display: flex;
flex-wrap: wrap;
.item {
width: 50%;
line-height: 13pt;
.label {
font-weight: bold;
      }
    }
.row {
width: 100%;
line-height: 13pt;
.label {
font-weight: bold;
      }
    }
  }
.table {
margin: 2ptauto;
.ant-table-thead {
.ant-table-cell {
font-weight: bold;
      }
    }
.ant-table-cell {
font-size: 8pt!important;
    }
  }
}

(5)使用起来也非常便捷,例子如下:

importReactfrom'react';
import { Button } from'antd';
importuseHtml2Pdffrom'@/components/useHtml2Pdf';
importTemplatefrom'@/templates/Template.jsx';
// 使用constHowToUse= () => {
const { print, loading } =useHtml2Pdf();
consthandleClick= () => {
// mock数据constdata= {
nameAndCardNoZhHant: '张三',
applyCodeZhHant: 'ABC123456789',
payTimeZhHant: '2022-12-12 12:12:12',
activityNameZhHant: '某某收费项',
total: 1,
discount: '2.00',
amount: '5.00',
payTypeZhHant: '支付宝',
nowDateZhHant: '2022-14-14 14:14:14',
    };
// 使用print({ Template: Template, options: data });
  };
return (
<Buttonloading={loading} onClick={handleClick}>点击打印</Button>  )
};
exportdefaultHowToUse;

(6)效果图如下:

A1E47F70-7D06-42cf-87D4-25974CAEC408.png

相关文章
|
10天前
|
前端开发 JavaScript 开发者
颠覆传统:React框架如何引领前端开发的革命性变革
【10月更文挑战第32天】本文以问答形式探讨了React框架的特性和应用。React是一款由Facebook推出的JavaScript库,以其虚拟DOM机制和组件化设计,成为构建高性能单页面应用的理想选择。文章介绍了如何开始一个React项目、组件化思想的体现、性能优化方法、表单处理及路由实现等内容,帮助开发者更好地理解和使用React。
36 9
|
15天前
|
前端开发 JavaScript Android开发
前端框架趋势:React Native在跨平台开发中的优势与挑战
【10月更文挑战第27天】React Native 是跨平台开发领域的佼佼者,凭借其独特的跨平台能力和高效的开发体验,成为许多开发者的首选。本文探讨了 React Native 的优势与挑战,包括跨平台开发能力、原生组件渲染、性能优化及调试复杂性等问题,并通过代码示例展示了其实际应用。
43 2
|
17天前
|
前端开发 JavaScript 开发者
React与Vue:前端框架的巅峰对决与选择策略
【10月更文挑战第23天】React与Vue:前端框架的巅峰对决与选择策略
|
20天前
|
前端开发 JavaScript
除了 jsPDF,还有哪些前端库可以用于生成 PDF?
【10月更文挑战第21天】这些前端库都有各自的特点和优势,你可以根据具体的项目需求、技术栈以及对功能的要求来选择合适的库。不同的库在使用方法、性能表现以及功能支持上可能会有所差异,需要根据实际情况进行评估和选择。
|
17天前
|
前端开发 JavaScript 开发者
“揭秘React Hooks的神秘面纱:如何掌握这些改变游戏规则的超能力以打造无敌前端应用”
【10月更文挑战第25天】React Hooks 自 2018 年推出以来,已成为 React 功能组件的重要组成部分。本文全面解析了 React Hooks 的核心概念,包括 `useState` 和 `useEffect` 的使用方法,并提供了最佳实践,如避免过度使用 Hooks、保持 Hooks 调用顺序一致、使用 `useReducer` 管理复杂状态逻辑、自定义 Hooks 封装复用逻辑等,帮助开发者更高效地使用 Hooks,构建健壮且易于维护的 React 应用。
28 2
|
17天前
|
前端开发 JavaScript 数据管理
React与Vue:两大前端框架的较量与选择策略
【10月更文挑战第23天】React与Vue:两大前端框架的较量与选择策略
|
22天前
|
JavaScript 前端开发 算法
前端优化之超大数组更新:深入分析Vue/React/Svelte的更新渲染策略
本文对比了 Vue、React 和 Svelte 在数组渲染方面的实现方式和优缺点,探讨了它们与直接操作 DOM 的差异及 Web Components 的实现方式。Vue 通过响应式系统自动管理数据变化,React 利用虚拟 DOM 和 `diffing` 算法优化更新,Svelte 通过编译时优化提升性能。文章还介绍了数组更新的优化策略,如使用 `key`、分片渲染、虚拟滚动等,帮助开发者在处理大型数组时提升性能。总结指出,选择合适的框架应根据项目复杂度和性能需求来决定。
|
22天前
|
缓存 前端开发 JavaScript
前端serverless探索之组件单独部署时,利用rxjs实现业务状态与vue-react-angular等框架的响应式状态映射
本文深入探讨了如何将RxJS与Vue、React、Angular三大前端框架进行集成,通过抽象出辅助方法`useRx`和`pushPipe`,实现跨框架的状态管理。具体介绍了各框架的响应式机制,展示了如何将RxJS的Observable对象转化为框架的响应式数据,并通过示例代码演示了使用方法。此外,还讨论了全局状态源与WebComponent的部署优化,以及一些实践中的改进点。这些方法不仅简化了异步编程,还提升了代码的可读性和可维护性。
|
16天前
|
前端开发 Android开发 开发者
前端框架趋势:React Native在跨平台开发中的优势与挑战
【10月更文挑战第26天】近年来,React Native凭借其跨平台开发能力在移动应用开发领域迅速崛起。本文将探讨React Native的优势与挑战,并通过示例代码展示其应用实践。React Native允许开发者使用同一套代码库同时构建iOS和Android应用,提高开发效率,降低维护成本。它具备接近原生应用的性能和用户体验,但也面临平台差异、原生功能支持和第三方库兼容性等挑战。
28 0
|
17天前
|
前端开发 JavaScript 开发者
React与Vue:前端框架的巅峰对决与选择策略
【10月更文挑战第23天】 React与Vue:前端框架的巅峰对决与选择策略