前端实现根据文件url批量打包为压缩包

问题背景

项目使用的 vue2data 中存在一个 materialsList 对象,computed 中存在一个 materialsObj 对象(根据 materialsList 计算出来的),数据内容为:

{
    "materialsList": [
      {"code":"CM","name":"A"},
      {"code":"EM","name":"B"},
      {"code":"LD","name":"C"},
      {"code":"MM","name":"D"},
      {"code":"NE","name":"E"},
      {"code":"OO","name":"F"},
      {"code":"PO","name":"G"},
      {"code":"RP","name":"H"},
      {"code":"WD","name":"I"},
      {"code":"WM","name":"J"}
    ],
  "materialsObj": {
    "CM":[],
    "EM":[{
      "attachment":{
        "url":"http://xxx.com/1640830559688.pdf"
      }
    }],
    "LD":[],
    "MM":[{
      "attachment":{
        "url":"http://xxx.com/1637292330025.pdf"
       }
    }],
    "NE":[{
      "attachment":{
        "url":"http://xxx.com/1637201758691.pdf"
      }
    }],
    "OO":[],
    "PO":[],
    "RP":[],
    "WD":[{
      "attachment":{
        "url":"http://xxx.com/1635848729053.pdf"
      }
    }],
    "WM":[{
      "attachment":{
        "url":"http://xxx.com/1628489193294.pdf"
      }
    }]
  }
} 

最后要生成一个压缩包,解压后内容为:

myFileName/
├── B/
│   └── materialsObj.EM 中的所有文件
├── D/
│   └── materialsObj.MM 中的所有文件
├── E/
│   └── materialsObj.NM 中的所有文件
├── I/
│   └── materialsObj.WD 中的所有文件
└── J/
    └── materialsObj.WM 中的所有文件

不要问我为什么这个要让前端来实现,后端说为了缓解服务器压力

技术实现

幸运的是,我发现了个前端生成 zip 压缩包的库:JSzip,具体见官网:https://stuk.github.io/jszip/

首先在组件中引入相关库:

import JSZip from 'jszip'
import axios from 'axios'

然后定义一个方法,用于用户点击"打包"按钮时执行:

packAll() {
  const fileObj = {} // 这个对象中的每一项的键名都是之后打包结果中的一个文件夹名,对应的键值就是文件夹内的文件
  Object.keys(this.materialsObj).forEach(key => {
    // 取出有文件的
    if (this.materialsObj[key].length > 0) {
      fileObj[this.materialsList.find(item => item.code == key).name] = this.materialsObj[key].map(item => {
        return {
          // 替换链接中最后一个"/"前面的内容
          // 因为项目文件放在腾讯云服务器中http://xxx.com,所以部署后,前端项目地址和其不一致,所以会出现跨域问题,现在我本地调试的
          // 时候,就将前缀替换为 /dev-file, 然后在配置一下本地代理,本地就可以调试了,但这时候线上肯定还是不行的,后端说一会儿给我
          // 提供一个接口,我把 http://xxx.com/1637292330025.pdf 文件地址传给他,他再直接给我用流的方式返回对应的腾讯云服务器中存储
          // 的文件,其实就是后续用我们线上部署的服务器做一个中间代理,这和我本地调试时候开代理是一样的
          url: item.attachment.url.replace(item.attachment.url.match(/.*\//)[0].slice(0, -1), '/dev-file'),
          fileName: item.attachment.fileName // 文件名
        }
      })
    }
  })
  // console.log('fileObj', fileObj)
  if (Object.keys(fileObj).length <= 0) {
    this.$Message.error('当前任务不存在附件')
  } else {
    this.loading = true // 按钮加载中样式
    const zip = new JSZip()
    let promiseAllNum = 0 // 记录当前执行的是第几次 Promise.all,往后看就懂了
    Object.keys(fileObj).forEach(async key => { // 遍历 fileObj 的键
      // 添加空文件夹
      const folder = zip.folder(key); // 创建文件夹,此时 key 就作为了打包结果中文件夹的名称
      // console.log('folder:', folder);
      // 由于数据结构的原因,我只能针对每个文件夹创建一个 Promise.all,那么为了知道到底什么时候所有文件夹都被打包完毕,这里才使用了
      // 下面的方式来执行,promiseAllNum 这个变量就很重要了,用于在 then 中判断当前执行的 Promise.all 是第几个,当最后一个执行完毕后
      // 才进行最终的打包操作
        const responses = await Promise.all(fileObj[key].map(item => axios.get(item.url, { responseType: 'blob' })))

        responses.forEach((res, index) => {
          const fileName = fileObj[key][index].fileName
          folder.file(fileName, res.data)
        })

        promiseAllNum += 1 // 每次 Promise.all 执行完,即每完成一次文件夹的创建,+1
        // 在所有请求结束后(当前打包的是最后一个文件夹),进行后续的操作,如生成压缩包等。
        if (promiseAllNum == Object.keys(fileObj).length) {
          // 生成并下载压缩包
          zip.generateAsync({ type: 'blob' }).then((content) => {
            const downloadUrl = window.URL.createObjectURL(content)
            const link = document.createElement('a')
            link.href = downloadUrl
            link.setAttribute('download', `${this.$attrs['task-title']}相关附件.zip`)
            document.body.appendChild(link)
            link.click()
            // 释放 URL 对象
            URL.revokeObjectURL(link.href)
            this.loading = false
          })
        }
     })
  }
}

chatgpt 对我代码的解释:

这段代码使用了 JSZip 库和 axios 库,实现了异步请求多个文件,并将它们打包成一个 ZIP 压缩文件并下载。具体来说,代码中的 fileObj 对象表示一个带有目录结构的文件列表,其中每个文件都包含一个 URL 和一个文件名字,代码首先循环遍历 fileObj 中的每一个目录,并在 ZIP 文件对象 zip 中创建对应的空文件夹。

随后,通过调用 Promise.all() 方法,将所有文件的下载请求合并为一个 Promise,等待所有请求结束后才继续执行操作。在 Promise.all() 的回调函数中,遍历所有响应结果 responses,将文件添加到对应的文件夹中。当所有请求结束后,通过调用 zip.generateAsync() 方法生成 ZIP 压缩文件,然后创建一个下载链接,将该链接加入到 DOM 中以触发下载。最后释放链接的 URL 对象。

需要注意的是,在上述过程中,使用了 async/await 关键字。这种方式可以保证所有异步请求都得到处理,并在所有请求结束后再进行下一步操作,使得代码更加优雅和易于理解。

本地调试时定义的代理配置:

proxyTable: {
  '/api': {
    target: 'https://dev.cn',
    secure: false,
    changeOrigin: true,
    pathRewrite: {
      '^/api': '' // 需要rewrite的,
    }
  },
  '/dev-file': {
    target: 'http://xxxx.com', // 腾讯云服务器地址
    secure: false,
    changeOrigin: true,
    pathRewrite: {
      '^/dev-file': ''
    }
  }
}

目前缺陷

  1. 本地虽配置了代理,但这样到线上肯定是不行的,这个就等后端提供接口,后续就可以将部署服务器作为中间代理去请求腾讯云服务器的资源了

  2. 现在是单独又在组件内引入了 axios,这个有点不合理,其实这个是为了新创建一个 前缀统一为 /dev-file 的请求实例,之后后端提供接口后,就用项目中统一创建的 axios 实例就行了,本地开发代理的话就走本地原来就有的对前缀 /api 的代理设置就行

问题修复

修复后代码:

 packAll() {
   const fileObj = {}
   Object.keys(this.materialsObj).forEach(key => {
     if (key !== 'upload' && this.materialsObj[key].length > 0) {
       fileObj[this.materialsList.find(item => item.code == key).name] = this.materialsObj[key].map(item => {
         return {
           attachmentId: item.attachmentId, // 后端提供接口获取单个文件了,需要这个文件id
           fileName: item.attachment.fileName 
         }
       })
     }
   })
   console.log('fileObj', fileObj)
   if (Object.keys(fileObj).length <= 0) {
     this.$Message.error('当前任务不存在附件')
   } else {
     this.$emit('update:loading', true)
     const zip = new JSZip()
     let promiseAllNum = 0
     Object.keys(fileObj).forEach(async key => {
       // 添加空文件夹
       const folder = zip.folder(key);
       // console.log('folder:', folder);
       // urlConfig.baseUrl 就是全局的api前缀
         const responses = await Promise.all(fileObj[key].map(item => axios.get(`${urlConfig.baseUrl}/base/attachment/file-cors?attachmentId=${item.attachmentId}`, { responseType: 'blob' })))

         responses.forEach((res, index) => {
           const fileName = fileObj[key][index].fileName
           folder.file(fileName, res.data)
         })

         promiseAllNum += 1
         // 在所有请求结束后,进行后续的操作,如生成压缩包等。
         if (promiseAllNum == Object.keys(fileObj).length) {
           // 生成并下载压缩包
           zip.generateAsync({ type: 'blob' }).then((content) => {
             const downloadUrl = window.URL.createObjectURL(content)
             const link = document.createElement('a')
             link.href = downloadUrl
             link.setAttribute('download', `${this.$attrs['task-title']}相关附件.zip`)
             document.body.appendChild(link)
             link.click()
             // 释放 URL 对象
             URL.revokeObjectURL(link.href)
             this.$emit('update:loading', false)
           })
         }
       })
   }
 }

这样就解决之前的第一个问题了,第二个问题感觉也没必要解决了

@2023/07/03优化

因后端说每个文件都要过后台接口,这里一次性会下载几十个文件,有些文件还很大,所以后台服务会崩,于是让我想办法不经过后台接口转发,能够线上解决跨域问题,从而将文件下载压力放到腾讯那边,所以我又优化了一下,优化后代码如下:

packAll() {
  const fileObj = {}
  Object.keys(this.materialsObj).forEach(key => {
    if (key !== 'upload' && this.materialsObj[key].length > 0) {
      fileObj[this.materialsList.find(item => item.code == key).name] = this.materialsObj[key].map(item => {
        return {
          // 添加一个 url 属性,其值为将 文件完整地址的腾讯云存储桶服务器前缀统一替换为 /getTencentFile
          url: item.attachment.url.replace(item.attachment.url.match(/.*\//)[0].slice(0, -1), '/getTencentFile'),
          attachmentId: item.attachmentId,
          fileName: item.attachment.fileName
        }
      })
    }
  })
  console.log('fileObj', fileObj)
  if (Object.keys(fileObj).length <= 0) {
    this.$Message.error('当前任务不存在附件')
  } else {
    this.$emit('update:loading', true)
    const zip = new JSZip()
    let promiseAllNum = 0
    Object.keys(fileObj).forEach(async key => {
      // 添加空文件夹
      const folder = zip.folder(key == '在先判决/相关案例' ? '在先判决-相关案例' : key);
      // console.log('folder:', folder);
        // 这里不再通过后端接口做转发,直接请求前缀为 /getTencentFile 的文件
        const responses = await Promise.all(fileObj[key].map(item => axios.get(item.url, { responseType: 'blob' })))
        responses.forEach((res, index) => {
          const fileName = fileObj[key][index].fileName
          folder.file(fileName, res.data)
        })

        promiseAllNum += 1
        // 在所有请求结束后,进行后续的操作,如生成压缩包等。
        if (promiseAllNum == Object.keys(fileObj).length) {
          // 生成并下载压缩包
          zip.generateAsync({ type: 'blob' }).then((content) => {
            const downloadUrl = window.URL.createObjectURL(content)
            const link = document.createElement('a')
            link.href = downloadUrl
            link.setAttribute('download', `${this.$attrs['task-title']}相关附件.zip`)
            document.body.appendChild(link)
            link.click()
            // 释放 URL 对象
            URL.revokeObjectURL(link.href)
            this.$emit('update:loading', false)
          })
        }
      })
  }
}

然后本地调试时需要配置代理:

proxyTable: {
  '/getTencentFile': {
    target: 'http://dev-public-xxxxxxx.cos.ap-guangzhou.myqcloud.com', 
    secure: false,
    changeOrigin: true,
    pathRewrite: {
       '^/getTencentFile': '' // 需要rewrite的,
    }
  }
},

但是上面只实现了本地调试,要实现测试/生产环境这些请求也能被代理过去,需要配置 Nginx 代理:

location /getTencentFile/ {
  proxy_pass http://dev-public-xxxxxxx.cos.ap-guangzhou.myqcloud.com/;
}

这样就能实现线上环境将以 /getTencentFile 开头的请求,代理到腾讯存储桶服务器上了。

本次代理配置参考了:

使用 Nginx 代理解决前端开发的跨域问题 - 掘金

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

鹏北海-RemHusband

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值