jszip的基本用法及自已项目需求下的灵活运用和感悟
前言
- 网页端操作,将一堆文件批量打包成一个压缩包一次性下载给到用户, 现成的插件可以用jszip, 需要了解底层可以自行阅读源码
- 这里记录jszip的基本用法及自已项目需求下的灵活运用和感悟
场景
- 需要打包的文件分成两类, 分装到两个文件夹中, 其中一类是后台拿到的文件地址类直接访问获取的文件, 另一类是前端渲染页面, 然后用pdf插件将dom生成pdf文件, 这两类文件的获取、处理、写入压缩包的流程都有区别
解决
安装
- jszip只是把文件处理成zip,但是还没有下载给到用户,还需要FileSaver配合处理才能实现打包并下载
npm i -s jszip
npm i -s file-saver
导入
jszip基本使用方法
- 众所周知,压缩包里肯定是可以有文件和文件夹的,而写这两种的方式也不同
JSzip.file(filePath, fileContent(, options))
: 写文件
第一个参数是filePath,文件路径,注意不是fileName文件名,有什么区别呢?比如传'txt/test.txt'和'test.txt'都是可以的,但是意义却不一样,前者表示在根目录的txt文件夹下写入一个test.txt文件,后者则是表示在根目录下写入一个test.txt文件
第二个参数是fileContent,文件内容,可供选择的内容形式很多,String、Blob、ArrayBuffer等都可以,也就意味着可以是直接写文本内容,可以直接带文件实例的内容,也可以是异步处理的二进制内容。
第三个参数options是zip压缩包参数配置,非必须,这里不细讲
栗子:const zip = new JSzip()
// 直接写文本
zip.file('file1.txt', '这是打开文件后看到的文本内容')
// 获取网页上传文件input的文件内容
var file = document.getElementById("fileID")
zip.file(file.files[0].name, file.files[0])
// 二进制
const fileUrl = 'https://it.is.fake.com/test.pdf'
axios.get(fileUrl, {
responseType: 'arraybuffer'
}).then(res => {
zip.file('asyncFile.pdf', res.data)
})
JSzip.folder(folderName)
: 写文件夹
这个相对file就比较单纯,就是写一个文件夹,但是它可以是链式的,从而实现套层
栗子:const zip1 = new JSzip()
zip1.folder('folder1').folder('folder2').file('file2.txt', 'content')
zip1.folder('folder1').file('file1.txt', 'content')
zip1.folder('folder1').file('file11.txt', 'content')
上述栗子的结果就是,在根目录下有一个folder1文件夹,folder1里有folder2文件夹和两个txt文件,folder2里又有一个file2.txt文件,可以看到,folder()的返回值是JSzip,所以可以链式,而file之后就不能继续了。而要往同一个folder里塞文件或者文件夹,就要逐次操作。
- 写入到zip文件并下载,
JSzip.generateAsync()
:
上面栗子中的zip和zip1接着用,栗子zip中用到了异步,所以写zip肯定是在异步处理之后,实际情况也是异步居多
所以这里我们把每个写文件任务都用Promise封装起来,用一个promises暂存所有promise任务,开启所有任务之后,用promises监听所有任务的完成Promise.all(promises).then(res => {
zip.generateAsync({ // 生成二进制流
type: 'blob'
}).then(content => {
saveAs(content, '压缩包1.zip') // 保存zip文件
zip1.generateAsync({
type: 'blob'
}).then(content2 => {
saveAs(content2, '压缩包2.zip')
})
})
})
实际场景中的使用及感悟
- 对所有即将写入zip的文件的生成过程的promise封装处理
methods: {
generateFilesByDom (dom, fileName) { // 传dom生成当前pdf
return new Promise((resolve, reject) => {
// 给50ms延迟是为了保证报告渲染好之后再resolve,不然会是没有数据的页面
setTimeout(() => {
getPdfBlobByDom(dom, fileName).then(res => { // 这里是额外封装的用jspdf生成pdf文件的函数
resolve(res)
})
}, 50)
})
},
// url型文件,后台给的一般情况下会是自己公司的存储桶里的存储对象,所以大概率是会跨域的,开发环境需要配置以下代理,并将url处理成代理前缀
// 生产环境则需要让后台配置nginx跨域,并保留url原型,不处理成开发环境用到的代理前缀
getUrlFile (url) { // 获取url型pdf文件
return new Promise((resolve, reject) => {
axios.get(url, {
responseType: 'arraybuffer'
}).then(res => {
resolve(res.data)
}).catch(err => {
reject(err.toString())
})
})
}
}
封装成promise之后,在每个调用它们的地方都保留promise任务const that = this
const promises = []
this.todolist1.forEach(item => {
const promise = that.getUrlFile(item.url).then(res => {
do sth...
})
promises.push(promise)
})
this.todolist2.forEach(item => {
const promise = that.generateFilesByDom(item.dom, item.fileName).then(res => {
do sth...
})
promises.push(promise)
})
// 此时promises就收录了所有要异步的promise任务
Promise.all(promises).then(res => {
// 写压缩包
})
- 用前端渲染的页面的dom,生成pdf,然后写入到zip中
- 首先,页面渲染的速度肯定是跟不上数据处理的速度的,那么在有多个dom要处理的时候,就不应该是只针对同一个dom进行数据的刷新从而使页面刷新,这样的话处理出来的pdf文件对应的数据肯定是错乱的;这也就意味着我们应该让一个pdf文件对应一个dom,进行单独渲染,每个dom之间互不干扰,数据才能正确。
- 所以将需要渲染的页面定义为一个组件pdfDom,方便调用;但还不够,我们并不是一开始就在父组件里初始化了一堆pdfDom,由于异步操作,我们并不知道任务量,不可能写死pdfDom的数量,所以肯定是对象实例化的形式在页面appendChild,处理完写入zip逻辑之后removeChild,这就涉及到如何appendChild一个vue组件。
- 那么现在的问题就是,如何给页面动态新增一个组件
const div = document.createElement('div') // 先给一个最外层的div,把整个组件移到页面外面看不到
div.style.cssText = 'position: absolute; top: -9999px; width: 800px;'
// 创建自定义的组件
const vm = new Vue({ // Vue实例
render (h) { // 这里的PdfDom就是导入的自定义组件
return h(PdfDom, { props: { compData: item.data } }) // props传递渲染数据
// 类似在html的写法 <PdfDom :compData="item.data"></PdfDom>
}
}).$mount() // 挂载
div.appendChild(vm.$el) // 重点,获取vue组件实例的dom是用vm.$el,实际append的也是这个$el
const comp = vm.$children[0] // 而这里的vm.$children[0]则是对应vue实例,如果需要调用实例里的函数,就要用这个,我这里是因为把初始化pdf写成了init(),所以需要在这里调用comp.init()
document.body.appendChild(div) // 给页面动态添加处理好的dom
// 然后初始化
comp.init()
- 至此,就只剩把dom处理成pdf,然后remove掉了
上面的generateFilesByDom传入动态的div,然后用jspdf处理就好了,放一下代码// html2pdf.js
import html2Canvas from 'html2canvas'
import JsPDF from 'jspdf'
export function getPdfBlobByDom (dom, fileName) {
const option = {
dpi: window.devicePixelRatio * 2, // 提高清晰度, 但是为了减少批量操作的时间和文件大小, 这里scale是2, 没有上面的清晰
scale: 2,
allowTaint: true
}
return new Promise((resolve, reject) => {
html2Canvas(dom, option).then(function (canvas) {
// const context = canvas.getContext('2d')
// context.scale(2, 2)
const contentWidth = canvas.width
const contentHeight = canvas.height
// 这里的592.28 * 841.89是pdf A4的尺寸,不能更改
const pageHeight = contentWidth / 592.28 * 841.89
let leftHeight = contentHeight
let position = 0
const imgWidth = 595.28
const imgHeight = 592.28 / contentWidth * contentHeight
const pageData = canvas.toDataURL('image/jpeg', 2.0)
const PDF = new JsPDF('', 'pt', 'a4')
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 -= 841.89
if (leftHeight > 0) {
PDF.addPage()
}
}
}
const file = base64ConvertFile(PDF.output('dataurlstring'), fileName)
resolve(file)
})
})
}
// base64转file文件
function base64ConvertFile (urlData, filename) { // 64转file
var arr = urlData.split(',')
var type = arr[0].match(/:(.*?);/)[1]
var fileExt = type.split('/')[1]
var bstr = atob(arr[1])
var n = bstr.length
var u8arr = new Uint8Array(n)
while (n--) {
u8arr[n] = bstr.charCodeAt(n)
}
return new File([u8arr], filename + '.' + fileExt, {
type: type
})
}
- 核心内容差不多就这些了