当产品说要批量上传8W个文件的时候
背景
最近一星期需求终于缓了下来,所以就准备对之前做的需求进行优化。在之前版本需求中,测试发现在批量上传的时候,数目一旦过多,并发请求太多导致浏览器崩溃。更可恶的是,产品已经准备好8W张图片准备一下子上传了,(我TM刀呢?)这样的话我这个小组件必然是撑不住这么多的并发的,也是无奈必须要对其进行优化。
现状分析
1.此时上传组件是直接使用element-ui的upload组件进行开发的;
2.根据业务需要,文件需要上传到OSS服务器上,因此在上传前需要
-
请求接口读取上传的配置(oss的上传地址以及一些上传参数);
-
请求接口去给每一个资源创建目录;
-
执行上传请求;
-
上传完成后再次请求后端接口将文件入库。
3.这样一算起来,每一个资源在上传的时候,都会有4个请求的并发,在element这个组件没有队列的情况下,批量上传的时候会崩溃可想而知。
优化分析
每一个资源上传都需要请求4个接口,并且在批量选中的时候这些请求都会并发执行,这就导致了请求崩溃的主要原因。以下是我能想到的优化点:
- 每次上传的请求是否可以减少,考虑到执行上传请求和入库接口都不能被省略,只能考虑减少前两步请求;
- oss的配置只在 created生命周期中请求一次,并不需要每次选择文件都去读取一次配置。
- 第二步和后端交流中了解到,资源的目录创建就是接口返回固定的路径名称+生成一个的UUID作为资源在oss上的目录,那将生成UUID放在前端也就不用去请求接口让后端生成了,我们只要保证上传到oss的UUID和入库时候相同就可以了。
- 上传文件能否是一个队列,在上一个文件上传完成后再执行下一个文件的上传;
- 记录添加的文件数量、成功上传数量和失败数量,方便上传结束后再去刷新列表;
- 8W个文件形成的列表,采用虚拟dom,避免页面dom元素过多引起页面卡顿。
具体实现
准备工作
- 考虑到需要队列上传,作者就采用了之前使用过的vue-simple-uploader这个上传轮子进行开发,这个插件还可以支持分片上传、秒传、断点续传等上传功能,非常的强大。vue-simple-uploader中有 simultaneousUploads的配置字段,表示默认同时上传的个数。
- UUID的生成就使用前端库uuid来生成。
- 虚拟dom作者采用vue-virtual-scroll-list来实现。
代码实现
html结构
<uploader
v-if="isPageReady"
:options="options"
class="uploader-example"
:autoStart="false"
multiple
ref="uploadVue"
@file-added="onFileAdded"
@file-success="onFileSuccess"
@file-progress="onFileProgress"
@file-error="onFileError"
>
<uploader-unsupport></uploader-unsupport>
<uploader-btn ref="uploadBtn"></uploader-btn>
<uploader-list v-show="fileListNumber && showUploadFile">
<template slot-scope="props">
<div class="title flex-vertical-center">
<div v-if="fileListNumber !== successNumber + failNumber">
<i
class="el-icon-upload"
style="font-size: 20px; color: #1890ff"
></i>
<span style="margin-left: 20px"
>上传中 {{ successNumber }}/{{ fileListNumber }}</span
>
</div>
<div v-else>
<span class="flex-vertical-center">
<i style="font-size: 16px" class="iconfont iconwancheng"></i>
<span style="margin-left: 10px">全部上传成功</span>
</span>
</div>
<i
v-if="!showFileList"
@click="changeShowFileList"
class="el-icon-arrow-down"
></i>
<i v-else @click="changeShowFileList" class="el-icon-arrow-up"></i>
<i @click="showUploadFile = false" class="iconfont iconguanbi"></i>
</div>
<div class="list" v-show="showFileList">
<div v-for="file in props.fileList" :key="file.id">
<uploader-file :file="file" :list="true">
<template slot-scope="props">
<div class="file-list">
<div class="file-img">
<img :src="file.name | dealImg" />
</div>
<div class="item">
<div class="name">{{ file.name }}</div>
<div class="size">{{ file.size | dealFileSize }}</div>
</div>
<el-button
@click="linkToDetail(file)"
v-if="file.flag"
type="text"
style="margin-right: 10px"
>查看</el-button
>
<el-progress
v-if="props.progress * 100 != 100"
type="circle"
:width="16"
:stroke-width="1"
:show-text="false"
:percentage="parseInt(props.progress * 100)"
></el-progress>
<i v-else class="iconfont iconwancheng"></i>
</div>
</template>
</uploader-file>
</div>
</div>
</template>
</uploader-list>
</uploader>
注意v-if=“isPageReady”,上文提到我们需要在created生命周期中获取上传的配置,所以在请求完配置接口后再进行实例化组件
import { v4 as uuidv4 } from "uuid";
data() {
return {
isPageReady: false,
FileLimit: {
FileMaxSize: 1, // 允许上传的大小 单位(G)
FileUnit: "G", // 单位
},
showUploadFile: true,
fileListNumber: 0,
successNumber: 0,
failNumber: 0,
listArr: [],
};
},
created() {
Promise.all([this.getOssUploadParameter()]).then((res) => {
this.isPageReady = true;
this.$nextTick(() => {
//vue-simple-upload内自己生成唯一值的方法会有重复,所以将uuid作为file的唯一值
this.$refs.uploadVue.uploader.opts.generateUniqueIdentifier = () => {
return uuidv4();
};
//vue-simple-upload 上传动态参数设置,如果是固定参数直接在query里面定义
this.$refs.uploadVue.uploader.opts.processParams = (par) => {
let obj = this.listArr.find(
(item) => item.identifier === par.identifier
);
//自定义上传参数
par.name = obj.name;
par.key = obj.key;
return par;
};
});
});
},
methods:{
// 文件添加事件
onFileAdded(file) {
//业务需要,显示上传的文件列表
if (!this.showUploadFile) {
this.showUploadFile = true;
}
file.pause();
if (file.isFolder) {
this.$message.error("不支持文件夹上传");
return false;
}
// 文件大小判断
const isSize = this.checkSize(file.size);
if (!isSize) {
file.ignored = true; // 过滤文件
file.cancel(); // 停止上传
return false;
}
this.fileListNumber++;
this.listArr.push({
identifier: file.uniqueIdentifier,//此时这里的uniqueIdentifier 就是我们自己生成的uuid
name: encodeURI(file.name),//防止+等特殊字符上传oss会有编码问题,所以编码一下
key: decodeURIComponent(
this.key + "/" + file.uniqueIdentifier + "/${filename}"
),
});
this.$nextTick(() => {
file.resume();
});
},
//获取上传配置
getOssUploadParameter() {
let that = this;
return new Promise(function (resolve, reject) {
that
.$get("upload/GetOssUploadParameter")
.then((res) => {
if (res && res.code == "0") {
that.options = {
query: {
OSSAccessKeyId: res.data.OSSAccessKeyId,
policy: res.data.policy,
signature: res.data.signature,
success_action_status: 200,
},
target: res.data.uploadUrl,
fileParameterName: "file", //上传文件时文件的参数名,默认file
chunkSize: 1 * 1024 * 1024 * 1024, //1G一个分片
testChunks: false, //是否测试每个块是否在服务端已经上传了,主要用来实现秒传、跨浏览器上传等,默认 true
simultaneousUploads: 1, //默认同时上传的个数
allowDuplicateUploads: true, //如果说一个文件以及上传过了是否还允许再次上传。默认的话如果已经上传了,除非你移除了否则是不会再次重新上传的
};
resolve(true);
}
}
})
.catch((err) => {
reject();
console.log(err);
});
});
},
//上传错误
onFileError(rootFile, file, response, chunk) {
this.failNumber++;
// this.failFileList.push(rootFile);
},
// 检查文件大小
checkSize(fileSize) {
var FileLimit = this.FileLimit;
var isSize = fileSize / 1024 / 1024 / 1024 < FileLimit.FileMaxSize; // 单位(M)
if (!isSize) {
this.$message.warning(
" 文件大小不能大于" + FileLimit.FileMaxSize + FileLimit.FileUnit + "!"
);
return false;
}
return true;
},
//文件上传成功之后的回调
onFileSuccess(rootFile, file, response, chunk) {
let params = {
name: file.name,
resId: file.uniqueIdentifier,
fileSize: file.size,
};
//调用后台入库接口,将上传的文件入库
this.$post("upload/AddRes", params)
.then((res) => {
if (res.code == 0) {
this.successNumber++;
this.$set(file, "flag", true);
if (this.successNumber + this.failNumber == this.fileListNumber) {
//全部上传完成通知父组件事件
this.$emit("uploadEnd", {
data: res.data,
flag: true,
});
} else {
//当前文件上传完成通知父组件事件
this.$emit("uploadEnd", {
data: res.data,
flag: false,
});
}
} else {
this.$message.error(res.msg);
}
})
.catch((err) => {
console.log(err);
});
},
}
最终实现效果
大家可以看到请求终于不是2000个并发了,而是一个一个队列上传,至此,只要服务器不挂,8W个文件也可以批量上传。
最后,大家有什么不明白的可以在下方评论区留言,喜欢的小伙伴记得给作者点个赞哦ღ( ´・ᴗ・` )比心~