jszip基本使用及应用实例

前言

  • 网页端操作,将一堆文件批量打包成一个压缩包一次性下载给到用户, 现成的插件可以用jszip, 需要了解底层可以自行阅读源码
  • 这里记录jszip的基本用法及自已项目需求下的灵活运用和感悟

场景

  • 需要打包的文件分成两类, 分装到两个文件夹中, 其中一类是后台拿到的文件地址类直接访问获取的文件, 另一类是前端渲染页面, 然后用pdf插件将dom生成pdf文件, 这两类文件的获取、处理、写入压缩包的流程都有区别

解决

安装

  • jszip只是把文件处理成zip,但是还没有下载给到用户,还需要FileSaver配合处理才能实现打包并下载
    npm i -s jszip
    npm i -s file-saver

导入

  • FileSaver插件用到的是saveAs来实现下载
    import JSzip from 'jszip'
    import { saveAs } from '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
        })
      }
      
  • 核心内容差不多就这些了
posted @ 2022-09-02 19:41  Mizuki-Vone  阅读(7648)  评论(0编辑  收藏  举报