问题背景
项目使用的 vue2
,data
中存在一个 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': ''
}
}
}
目前缺陷
-
本地虽配置了代理,但这样到线上肯定是不行的,这个就等后端提供接口,后续就可以将部署服务器作为中间代理去请求腾讯云服务器的资源了
-
现在是单独又在组件内引入了
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
开头的请求,代理到腾讯存储桶服务器上了。
本次代理配置参考了: