最近写的项目需要支持多个大文件上传、显示上传进度条、所有文件上传状态、上传失败重新上传。前面三个还好实现,但是由于目前后端接口未实现断点续传,所以现在上传失败,点击重新上传是从头开始上传。这个功能还待定,但是对于前端来说实现难度不大。效果图如下:
实现思路
FileReader
FileReader
对象允许我们读取存储在用户计算机上文件的内容,使用 File
或者**Blob
** 对象指定要读取的文件或数据。大文件上传,在文件成功读取时 load
之后,我们只要知道这个文件大小是多少,按量分割,顺序上传,每次只上传当前这个区间单位的文件。
问题
分片上传有一个很重要的点就是当前上传的分片是否已经上传过?
如何判断当前上传的是第几片?
多个上传如何区别?
上传之前校验文件格式
javascript
async beforeUploadFile(file, val) {
await reportApi.abortMultipartUpload({ fileId: this.currentItem.fileId });
const ext = file.fileSuffixName || extendForFile(file);
if (
[
"doc",
"docx",
"pdf",
"xls",
"xlsx",
"zip",
"mp4",
"pptx",
"ppt",
"mp3",
].some((item) => item === ext)
) {
this.requestName = reportApi.ossFileUploadFile;
if (file.size / 1024 / 1024 / 1024 > 100) {
this.$message.error("文件大小超出100M,请重新上传!");
return false;
}
} else if (
["png", "bmp", "jpg", "jpeg", "gif", "tif"].some((item) => item === ext)
) {
this.requestName = reportApi.ossFileUploadFile;
if (file.size / 1024 / 1024 / 1024 > 40) {
this.$message.error("图片大小超出40M,请重新上传!");
return false;
}
} else {
this.$message.error("暂不支持文件上传格式,请重新上传!");
return false;
}
this.selectOptions = val;
console.log(file, "this.$refs.cropper.showModal");
}
上传文件解析
文件load成功后,需要先初始化文件,获取文件的切片大小、切片数量以及一些文件的唯一信息。虽然支持上传多个文件,但是上传还是按照顺序一个一个的,fileListData在每次初始化都需要记录当前文件的所有信息。上传进度只需要在初始化成功/失败、分片上传成功/失败,上传完成这5种情况下,记录当前上传分片数量。 在初始化成功后,根据当前上传分片数量和总数量大小比对,来判断是否要执行合并操作。
javascript
// 获取文件后缀
extendForFile(file){
var imgName = file.name
var idx = imgName.lastIndexOf('.')
var ext = ''
if (idx !== -1) {
ext = imgName.substr(idx + 1).toUpperCase()
ext = ext.toLowerCase()
}
return ext
}
javascript
uploadVideo({ file, onSuccess, onError }, prop) {
console.log(111111, this.fileListData);
const that = this;
let fileRederInstance = new FileReader();
fileRederInstance.readAsBinaryString(file);
fileRederInstance.addEventListener("load", (e) => {
let fileBolb = e.target.result;
const fileMD5 = MD5(fileBolb); // 文件秒传的关键,文件生成的md5是唯一的,如果判断上传的文件和数据库原来上传过的文件md5值相同就可以直接吐出原来的文件地址,达到文件秒传的效果
// 1、初始化
const ext = file.fileSuffixName || extendForFile(file);
this.fileListData.push({
fileName: file.name,
fileSize: file.size,
fileSuffixName: ext,
fileError: false,
size: file.size,
video: file,
chunkCount: Math.ceil(file.size / this.chunkSize), // 切片数量
fileMD5,
});
reportApi
.shardingInitiate({
fileName: file.name || "111",
md5: fileMD5,
})
.then((res) => {
this.fileListData.forEach((f) => {
if (f.fileName == file.name) {
f.size = file.size;
f.fileId = res.result.fileId;
f.uploadId = res.result.uploadId;
f.videoName = res.result.fileName;
f.fileDirVideoKey = res.result.fileDirVideoKey;
}
});
//初始化文件之后,需要触发父组件,开始记录上传状态和上传进度
this.onFinish(this.fileListData)
this.readChunkMD5(0, file.name);
})
.catch((error) => {
console.log(error, "09----");
this.fileListData.forEach((f) => {
if (f.fileName == file.name) {
f.fileHttpUrl = "";
f.fileUrl = "";
f.fileError = true;
f.fileResetUpload = true;
}
});
//初始化文件失败,需要触发父组件,记录上传状态
this.onFinish(this.fileListData)
});
});
},
javascript
getChunkInfo(currentChunk, curFileName) {
const cur = this.fileListData[0];
console.log(cur);
let start = currentChunk * this.chunkSize; // 起始位置
let end = Math.min(cur.size, start + this.chunkSize); // 结束位置
let chunk = cur.video.slice(start, end); // 切片内容
console.log(start, end, chunk);
console.log("0." + start, "start222");
// let that = this;
// let storeAs = this.videoName;
// that.$message.loading(`${storeAs}正在上传,请稍后...${start}%`,0);
return { start, end, chunk };
},
javascript
readChunkMD5(num, curFileName) {
const { chunk } = this.getChunkInfo(num, curFileName);
const cur = this.fileListData.find((f) => f.fileName == curFileName);
const curFileId = this.fileListData.find(
(f) => f.fileName == curFileName
).fileId;
console.log("readChunkMD5:", num)
this.onProgress({
currentChunk: num,
chunks: cur.chunkCount,
curFileId,
})
// 如果当前上传的文件小于总数量就执行上传操作,如果大于当前数量走合并操作
if (num < cur.chunkCount) {
let fetchForm = new FormData();
fetchForm.append("chunk", num + 1); // 当前分片数
fetchForm.append("chunks", cur.chunkCount);
fetchForm.append("file", chunk); // 当前分片文件内容
fetchForm.append("md5", cur.fileMD5);
fetchForm.append("objectName", cur.videoName);
fetchForm.append("uploadId", cur.uploadId);
fetchForm.append("curPartSize", chunk.size);
fetchForm.append("fileDirVideoKey", cur.fileDirVideoKey);
fetchForm.append("fileId", cur.fileId);
fetchForm.append("fileSize", cur.size);
reportApi
.shardingFile(fetchForm)
.then((res) => {
this.loadProgress++;
num = num + 1;
this.readChunkMD5(num, curFileName);
this.onFinish(this.fileListData)
})
.catch((error) => {
console.log(error, "09----");
this.fileListData.forEach((f, i) => {
if (i == 0) {
f.fileHttpUrl = "";
f.fileUrl = "";
f.fileError = true;
f.fileResetUpload = true;
}
});
//分片上传成功后,需要触发父组件,开始记录上传状态和上传进度
this.onFinish(this.fileListData);
this.$forceUpdate();
});
} else {
reportApi
.complete({
uploadId: cur.uploadId,
objectName: cur.videoName,
fileDirVideoKey: cur.fileDirVideoKey,
fileSize: cur.size,
})
.then((res) => {
this.fileListData.forEach((f, i) => {
if (i == 0) {
f.fileHttpUrl = res.result.fileHttp;
f.fileUrl = res.result.fileUrl;
f.fileError = false;
f.fileResetUpload = false;
f.uploadTime = moment().format("YYYY-MM-DD HH:mm:ss");
}
});
//上传成功后,需要触发父组件,开始记录上传状态和上传进度
this.onFinish(this.fileListData)
this.$emit("updateRender");
})
.catch((error) => {
console.log(error, "09----");
this.fileListData.forEach((f, i) => {
if (i == 0) {
f.fileHttpUrl = "";
f.fileUrl = "";
f.fileError = true;
f.fileResetUpload = true;
}
});
//分片上传失败后,需要触发父组件,开始记录上传状态和上传进度
this.onFinish(this.fileListData)
this.$emit("updateRender");
});
}
}
javascript
<a-upload
:accept="uploadfileType"
name="file"
:multiple="false"
:custom-request="uploadVideo"
:show-upload-list="false"
:before-upload="beforeUploadFile"
>
<slot name="content">
<div style="display:flex:align-items:center">
<span>上传失败,</span>
<span class="link">重新上传</span>
<img
:src="undo_blue"
width="12"
height="12"
>
</div>
</slot>
</a-upload>
<video
ref="video"
v-show="false"
/>
</div>
完整代码也贴上去了,等后端断点续存的接口出来。就可以在上传失败的情况下,根据当前上传到第几片,当前文件的uploadId继续上传。失败之前的文件数据只要不刷新页面是会继续保留的,所以这功能问题不大。 今日的分享完毕,下次在来~~