使用jspdf将DOM元素绘制成PDF文件
最近接到了一个需求,将页面绘制好的模板,绘制成pdf文件并传给后台。开始在网上文章中学到,使用html2Canvas+jspdf基本实现了。但是生成的pdf太模糊,要求重新做。经过几天,终于搞定。
现在总结一下。
基本思路是:
- jspdf将页面元素转换成二进制流
- 使用new Blob将二进制流转换成文件
- 使用new FormData将文件传给后台
将DOM元素绘制成二进制流
在插件绘制的方法中,这步非常简单,只需要调用html()函数即可。但是也是最费劲的,因为插件一开始并不支持中文,后来提供了加载字体库的方法也算是解决的了。时间有限,找到字体文件都非常大,使用官方提供的方式后,更大。个人工作中的项目是基于vue2.x开发的,将文件引入后,直接提示内存溢出。后来是用接口方式,将转换后的文件传过来才解决。所以,为了项目性能,正式开发是请做好以下准备。
一个传输base64格式字体的接口
将在jsPDF的官网github上克隆本地后,点击fontconverter>fontconverter.html将准备好的中文字库转换成.js文件,个人用的FZYTK.ttf(注意只支持.ttf格式的),经测试思源黑体也可以(不过转换后有40M,接口压力很大)。
将转换后的js文件中的‘var font = ’后面的base64取出后,放在一个文件后给后端开发。后端读取文件,通过接口给前端。
将字体存入本地
由于字体很大,传输不易。建议将字体存在浏览器本地。localstorage大小只有5M,所以建议使用es6新的indexDB进行存储,网上有多封装好的方法。存储和读取都非常方便。
安装jspdf
npm install jspdf –save or yarn add jspdf
准备完成,下面是代码时间。
import JsPDF from 'jspdf';// 引入插件
import axios from 'axios'
import { creatUpdateStore, addDataStore, getStoreData } from './indexDB'// 引入封装好的indexDB函数,creatUpdateStore:创建,addDataStore:添加,getStoreData:读取
let font = null;// 全局字体变量,个人是生成多个文件,用全局变量后,可以避免多次读取
creatUpdateStore('fonts');// 创建一个indexDB库,命名为font
const cretePDF = () => {
return new Promise(resolve => {
const PDF = new JsPDF('', 'pt', 'a4')// 创建一个PDF对象,大小可以自行调整,这里用的是a4纸
if (font === null) { // 先判断font是否有值
getStoreData('fonts', 1).then(res => {// font没有值,在查看本地是否有。获取indexDB方法有所差异,可自行调整。
if (res?.FZYTK) { //本地有字体
font = res.FZYTK
PDF.addFileToVFS('FZYTK.ttf', font)
PDF.addFont('FZYTK.ttf', 'FZYTK', 'normal')
PDF.setFont('FZYTK')
resolve(PDF)
} else { //本地无字体,通过接口获取
axios.get('XXX').then(response => {
font = response.data.data.replaceAll('', '')//后端读取文件可能会产生,直接替换掉。
addDataStore('fonts', { FZYTK: font })
PDF.addFileToVFS('FZYTK.ttf', font)
PDF.addFont('FZYTK.ttf', 'FZYTK', 'normal')
PDF.setFont('FZYTK')
resolve(PDF)
})
}
})
} else {//font 有值,直接设置字体
PDF.addFileToVFS('FZYTK.ttf', font)
PDF.addFont('FZYTK.ttf', 'FZYTK', 'normal')
PDF.setFont('FZYTK')
resolve(PDF)
}
})
}
// vue 全局函数。
Vue.prototype.getNodePdf = function (nodeId) {
loading = Loading.service({ //loading,如果需要的话
lock: true,
text: '正在生成pdf文件...',
background: 'rgba(0,500,200,.5)'
})
return new Promise((resolve, reject) => {// 生成一个promise,包装文件生成后,在进行下一步动作
try {
const node = document.querySelector(`#${nodeId}`) //获取要绘制的DOM
const clonedNode = node.cloneNode(true) // 克隆DOM,方便改变DOM的样式,不用克隆亦可
cretePDF().then(PDF => {// PDF对象生成后,
clonedNode.style.width = `${PDF.getPageWidth() - 20}px`、// 获取PDF大小,重新这是DOM大小,使DOM都能绘制在PDF文件上面。
PDF.html(clonedNode, {// 绘制pdf
callback: function (doc) {
const output = doc.output('arraybuffer')// 以流的方式返回结果。使用PDF.save('pdfName'),浏览器会将生成的PDF直接下载下来
resolve(output) // 返回结果。
},
// 开始从10,10坐标开始绘制
x: 10,
y: 10
})
})
} catch (e) {
reject(e)
} finally {
loading.close()//关闭loading
}
})
}
复制代码
pdf生成完成。
使用Blob将流转换成file文件
代码如下
/**
* @description:将bob转成file
* @param {string} DOM的id
* @param {name} 文件名称
*/
getPdfFn(id, name) {
if (id && name) {
return new Promise(resolve => {
this.getNodePdf(id, name).then(file => {
const pdfFile = new Blob([file], { type: 'application/pdf' })
const files = new window.File([pdfFile], `${name}.pdf`, { type: 'application/pdf' })
resolve(files)
})
})
}
}
复制代码
通过接口传输给后台
const _that = this
const formData = new FormData()
files.forEach((file, index) => { //files 为生成的pdf集合
formData.append(`files[${index}]`, file)
})
this.$axios.post('XXX', formData)
.then(function (res) {
if (res.data.code === 1000) {
_that.$message.success('提交成功')
_that.isEdit = false
} else {
_that.$message.error(res.data.msg)
}
})
复制代码
个人loading和message用的element-ui提供的,使用其他框架可自行调整。
© 版权声明
文章版权归作者所有,未经允许请勿转载。
THE END