概述:
一、上传组件部分:使用el-upload,在页面创建一个上传控件。
UI组件参数:multiple--是否支持多选文件、action--必选参数,上传的地址(因为实际情况下肯定会使用项目组件来调用后端接口,所以此处我给了action="",)、show-file-list--是否显示已上传文件列表、http-request--覆盖默认的上传行为,可以自定义上传的实现(一开始是使用before-upload来做上传文件之前的钩子,但是我们需要拦截action 属性的请求,做自定义文件的上传行为,官网上给出了“返回 false 或者返回 Promise 且被 reject,则停止上传”,但是我在return false后还是会在上传时默认调用一次空请求,再调用我的自定义上传请求接口,没办法只能将before-upload替换为http-request实现自定义上传)、limit--配合multiple使用,最大允许上传个数(这里有个坑要注意一下,上传后你会发现on-exceed的返回参数中,上传文件数是不断累积的,比如你限制最大同时上传10个文件,第一次你上传了7个,第二次你上传了5个,此时会提示你上传了12个,因此第二次被限制住了,所以需要在on-success中清除上传历史记录,防止文件限制莫名其妙的超出)、on-exceed--文件超出个数限制时的钩子、on-success--文件上传成功时的钩子。
<el-upload ref="upload" multiple action="" :show-file-list="false" :http-request="handleFileUpload" :limit="10" :on-exceed="handleExceed" :on-success="handleAvatarSuccess">
<Button class="button" type="primary" :disabled="disabled">上传</Button>
</el-upload>
二、JS部分:点击上传后触发自定义的上传方法,接收一个file参数为上传的文件,这里要注意两点,1.当同时上传多个文件时,会多次触发事件,有几个文件就执行几次方法,file参数为单次执行的文件对象而不是全部的上传文件;2.before-upload的返回参数直接就是文件对象,而http-request的返回参数需要file.flie才能到文件对象。
首先通过file.size获得文件大小,判断当前文件是否需要分片上传,过小的文件直接上传即可,此分支不再多讲。
npm安装spark-md5
npm install --save spark-md5
在当前文件引入
import SparkMD5 from "spark-md5";
为当前文件生成md5值(md5主要是给断点续传和快传用的,就是用来做个标识,具体原理这里就不讲了,只写前端业务实现。)正常是判断文件大小,如果不是很大的情况直接用整个文件生成md5值就行,当判断文件过大时,应该先用file.slice()分割文件,将第一片的md5值拼接最后一片的md5值,防止性能消耗过大。我这里就简单的和后端沟通一下,统一不做区分,直接全部用第一片的拼接最后一片的md5值。
文件完整性检测
在正式调用上传接口前先调用文件完整性检测接口,为断点续传用,后端通过md5值结合文件名来检测当前文件是否已经上传过,上传了多少,获取当前文件应该上传的分片的下标。
上传文件
file.slice()分割出本次上传的文件,同各种业务参数调用上传接口上传文件。在成功的回调中递归调用上传方法,每次重新分割下一片文件,反复上传直到全部上传完毕。(代码包含进度条部分)
// 上传(同时选择多个文件时会多次上传该文件)
async handleFileUpload(item) {
let file = item.file
this.disabled = true;
this.list = [...this.list, file]; //用于展示进度
this.uploadModal = true; // 展示进度弹窗
// 文件大于50MB时分片上传
if(file.size / 1024 / 1024 < 50){
const formData = new FormData();
formData.append("file", file);
formData.append("name", file.name);
this.$api.xxx(formData).then((res) => {
if (res.error !== "error") {
this.list.forEach((item)=>{
if(item.name === file.name){
item.percent = 100;
}
})
this.$Message.success(`${file.name}:上传完成`);
}else {
this.list.forEach((item)=>{
if(item.name === file.name){
item.typeProgress = 1;
}
})
}
}).finally(() => {
this.disabled = false;
});
}else{
const size = 10 * 1024 * 1024; // 10MB 每个分片大小
let current = 0; // 当前分片index(从0开始)
let total = Math.ceil(file.size / size); // 分片总数
let startByte = 0;
let that = this;
// 通过文件获取对应的md5值
let dataFileStart = file.slice(0, size); // 第一片文件
let dataFileEnd = file.slice(size* (total - 1) , file.size) // 最后一片文件
var spark = new SparkMD5.ArrayBuffer();
function getMd5(file) {
return new Promise((resolve) => {
// 对文件对象的处理
var fileReader = new FileReader();
fileReader.readAsArrayBuffer(file);
// fileReader.onload为异步函数,要放到Promise对象中,等待状态的变更后再返回生成的md5值
fileReader.onload = function (e) {
spark.append(e.target.result);
resolve(spark.end());
};
});
}
//获取文件二进制数据
let md5Start = await getMd5(dataFileStart); // 第一片的md5值
let md5End = await getMd5(dataFileEnd); // 最后一片的md5值
let md5 = md5Start + md5End;
// 文件完整性检测
this.$api.xxx({md5:md5,fileName:file.name}).then((res) => {
if (res.error !== "error") {
current = res?.indexOf("0") === -1?0 : res?.indexOf("0"); // 当前服务器应该上传的分片下标
startByte = size* current
uploadChunk();
}
}).finally(() => {
this.disabled = false;
});
// 编辑上传参数并上传文件
function uploadChunk() {
const formData = new FormData();
const endByte = Math.min(startByte + size, file.size);
const chunk = file.slice(startByte, endByte); // 当前分片文件
formData.append("file", chunk);
formData.append("name", file.name);
formData.append("current", current);
formData.append("total", total);
formData.append("md5", md5);
that.$api.xxx(formData).then((res) => {
if (res.error !== "error") {
that.list.forEach((item)=>{
if(item.name === file.name){
item.percent = Math.floor(((Number(current) + 1) * 100) / total);
}
})
that.list= [...that.list]
startByte = endByte;
if (startByte < file.size) {
current++;
uploadChunk();
} else {
that.$Message.success(`${file.name}:上传完成`);
}
}else {
that.list.forEach((item)=>{
if(item.name === file.name){
item.typeProgress = 1;
}
})
}
}).finally(() => {
that.disabled = false;
});
}
}
return false;
},
// 文件超出个数限制时的钩子
handleExceed(files) {
this.$Message.warning(`最多同时上传 10 个文件,本次选择了 ${files.length} 个文件`);
},
// 文件上传成功时的钩子(清除上传历史记录,防止文件限制超出)
handleAvatarSuccess(){
this.$refs.upload.clearFiles();
}