实现的效果:
1、多个大文件(支持10个G以上)分片上传
2、进度条展示进度
3、控制文件上传暂停和取消
实现关键点:
1、文件预处理(md5计算、请求和进度处理等)
2、分片上传的流程(查询已上传分片、文件合并等)
3、文件的暂停、开始、取消
文件预处理
首先使用file类型的input框获取文件,对文件进行深拷贝,再清空input的value值(防止input的change事件不被触发)。
let files = e.target.files;
let copiedFiles = []
for(let i = 0; i < files.length; i++){
copiedFiles.push(new File([files[i]], files[i].name, { type: files[i].type }))
}
this.$emit("bigFileChange", copiedFiles);
this.$refs.input.value = null;
对文件进行处理,核心思想是为每个文件构造一个对象,封装该文件的md5信息(用于标识该文件)和进度、请求、取消标识(用于文件的暂停)等信息。
async bigFileChange(files) {
// 新增的文件
let newFiles = [];
// 筛选出检验合格的文件
let okFileIndexs = this.checkRules(files);
for (let i = 0; i < okFileIndexs.length; i++) {
let fileIndex = okFileIndexs[i];
// 为文件构建对象
let fileObj = {};
fileObj.md5 = await this.firstChunkMd5(
files[fileIndex],
this.chunkSize
);
fileObj.progress = 0;
fileObj.isPaused = false;
// 查询该文件合并进度的轮询计时器
fileObj.mergeTimer = null;
fileObj.status = "上传中";
fileObj.newSize = this.getFileSize(files[fileIndex].size);
fileObj.file = files[fileIndex];
fileObj.category = this.category;
// 该文件的所有请求
fileObj.requests = [];
// 该文件的取消标识
fileObj.cancelTokens = [];
// 将构建的对象记录下来
newFiles.push(fileObj);
this.bigFileList.push(fileObj);
}
for (const newFileObj of newFiles) {
this.uploadBigAttachment(newFileObj);
}
}
计算md5值采用的是SparkMD5,为了减少计算量,采用文件的第一块的md5作为整个文件的md5。
firstChunkMd5(file, chunkSize) {
return new Promise((resolve, reject) => {
const fileReader = new FileReader();
const spark = new SparkMD5.ArrayBuffer();
const chunk = file.slice(0, chunkSize);
fileReader.onload = function (event) {
spark.append(event.target.result);
const md5 = spark.end();
resolve(md5);
};
fileReader.onerror = function () {
reject(new Error("File read error."));
};
fileReader.readAsArrayBuffer(chunk);
});
}
在界面上为每个文件创建进度条。
<div
class="bigFileProgress"
v-for="(f, index) in bigFileList"
:key="index"
>
<div class="bigFileTop">
<Tooltip :content="f.file.name" placement="top">
<div class="bigFileName">
{{ f.file.name }}
</div>
</Tooltip>
<div class="bigFileSize" style="width: 20%">
{{ f.newSize }}
</div>
<!-- <div class="bigFileUploadProgress" style="width: 5%">
{{ f.progress }}%
</div> -->
<div
class="bigFileStatus"
:style="{
width: '20%',
color: f.status == '上传失败' ? 'red' : 'black',
}"
>
{{ f.status }}
</div>
<div
class="bigFileActions"
style="position: relative; width: 20%"
>
<Button
@click="pauseBigFile(f)"
type="primary"
size="small"
style="position: absolute"
v-if="!f.isPaused"
:disabled="f.progress == 100"
>
暂停
</Button>
<Button
@click="restartBigFile(f)"
type="primary"
size="small"
:disabled="f.progress == 100"
:style="{ opacity: f.progress == 100 ? 0 : 1 }"
>
开始
</Button>
<Button
@click="cancelUpload(f)"
type="error"
size="small"
style="margin-left: 10px"
>
取消
</Button>
</div>
</div>
<div class="progress">
<Progress :percent="f.progress" :stroke-width="5"></Progress>
</div>
</div>
分片上传
首先查询文件已经上传的分片数,如果全部上传了,进度立即更新为100%(秒传),如果没完全上传,则上传未上传的分片并实时更新进度,各分片上传完毕后请求合并,采用轮询检测合并进度。
this.checkFile(fileObj, chunks)
.then(async (res) => {
console.log(res);
if (res.data.data.completed) {
// 如果当前文件已经上传成功 则无需继续上传
fileObj.progress = 100;
fileObj.status = "上传成功";
// 为成功上传的附件添加id
if (res.data.data.attachmentId) {
fileObj.attachmentId = res.data.data.attachmentId;
}
this.$forceUpdate(); // 强制重新渲染组件
this.$emit("fileUpdate");
} else {
// 当前文件没有上传成功
// 获取已经上传的分片数组
let uploadedChunks = res.data.data.uploadChunks;
// 获取当前的进度
let newProgress = parseInt(
(uploadedChunks.length / chunks) * 100
);
fileObj.progress = newProgress;
this.$forceUpdate(); // 强制重新渲染组件
// 文件均已上传完 但还未合并
if (res.data.data.canMerge || uploadedChunks.length == chunks) {
this.mergeBigFile(fileObj)
.then((res) => {
fileObj.status = "合并中";
this.$forceUpdate(); // 强制重新渲染组件
// 先清除该文件上次的合并计时器
if (fileObj.mergeTimer) {
clearInterval(fileObj.mergeTimer);
}
fileObj.mergeTimer = setInterval(() => {
this.getMergeProcess(fileObj).then((res) => {
if (res.data.data.completed) {
fileObj.status = "上传成功";
// 为成功上传的附件添加id
if (res.data.data.attachmentId) {
fileObj.attachmentId = res.data.data.attachmentId;
}
this.$forceUpdate(); // 强制重新渲染组件
// 合并完成
fileObj.requests = [];
fileObj.cancelTokens = [];
clearInterval(fileObj.mergeTimer);
this.$emit("fileUpdate");
}
});
}, 2000);
})
.catch((error) => {
console.error("上传失败:", error);
fileObj.status = "上传失败";
if (fileObj.mergeTimer) {
clearInterval(fileObj.mergeTimer);
}
this.$forceUpdate();
});
} else {
// 文件还没上传完
let currentChunk = 0;
// 上传没有上传的部分
while (currentChunk < chunks) {
if (!uploadedChunks.includes(currentChunk)) {
const start = currentChunk * this.chunkSize;
const end = Math.min(
start + this.chunkSize,
fileObj.file.size
);
const chunk = fileObj.file.slice(start, end);
// 构造该块的上传请求
const formData = new FormData();
let fileType = fileObj.file.name.substring(
fileObj.file.name.lastIndexOf(".") + 1
);
formData.append("fileName", fileObj.file.name);
formData.append("fileType", fileType);
formData.append("md5", fileObj.md5);
formData.append("category", fileObj.category);
formData.append("ownerType", "bill");
formData.append("ownerId", this.billidParam);
formData.append("chunkNum", currentChunk);
formData.append("chunkSize", this.chunkSize);
formData.append("chunkTotal", chunks);
formData.append("file", chunk);
// 该块的取消令牌
let cancelToken = axios.CancelToken.source();
fileObj.cancelTokens.push(cancelToken);
let request = GMS.$http.post(
"/bsp/bjgzw/attachment/uploadChunk",
formData,
{
headers: this.headers,
cancelToken: cancelToken.token,
}
);
fileObj.requests.push(request);
}
currentChunk++;
}
// 当前文件下的所有请求
for (let i = 0; i < fileObj.requests.length; i++) {
fileObj.requests[i]
.then((res) => {
console.log(res);
// 进行进度控制
let progress = parseInt(
(res.data.data.uploadChunks.length / chunks) * 100
);
if (progress > fileObj.progress) {
fileObj.progress = progress;
this.$forceUpdate(); // 强制重新渲染组件
}
// 进行文件的合并控制
if (res.data.data.canMerge) {
// 文件可以合并了
this.mergeBigFile(fileObj)
.then((res) => {
fileObj.status = "合并中";
this.$forceUpdate(); // 强制重新渲染组件
// 先清除该文件上次的合并计时器
if (fileObj.mergeTimer) {
clearInterval(fileObj.mergeTimer);
}
fileObj.mergeTimer = setInterval(() => {
this.getMergeProcess(fileObj).then((res) => {
if (res.data.data.completed) {
fileObj.status = "上传成功";
// 为成功上传的附件添加id
if (res.data.data.attachmentId) {
fileObj.attachmentId =
res.data.data.attachmentId;
}
this.$forceUpdate(); // 强制重新渲染组件
// 合并完成
fileObj.requests = [];
fileObj.cancelTokens = [];
clearInterval(fileObj.mergeTimer);
this.$emit("fileUpdate");
}
});
}, 2000);
})
.catch((error) => {
console.error("上传失败:", error);
fileObj.status = "上传失败";
if (fileObj.mergeTimer) {
clearInterval(fileObj.mergeTimer);
}
this.$forceUpdate();
});
}
})
.catch((error) => {
if (axios.isCancel(error)) {
console.log("上传已暂停或取消");
} else {
console.error("上传失败:", error);
fileObj.status = "上传失败";
this.$forceUpdate();
}
fileObj.requests = [];
fileObj.cancelTokens = [];
});
}
}
}
})
.catch((error) => {
console.error("Error:", error);
fileObj.status = "上传失败";
this.$forceUpdate();
});
上传暂停、开始、取消
暂停上传即根据取消标识将当前文件的所有请求进行取消。
pauseBigFile(fileObj) {
fileObj.cancelTokens.forEach((item) => {
item.cancel("上传暂停");
});
fileObj.isPaused = true;
fileObj.status = "已暂停";
this.$forceUpdate(); // 强制重新渲染组件
}
开始上传即对文件重新进行上传处理。
restartBigFile(fileObj) {
fileObj.isPaused = false;
fileObj.status = "上传中";
this.$forceUpdate(); // 强制重新渲染组件
fileObj.requests = [];
fileObj.cancelTokens = [];
this.uploadBigAttachment(fileObj);
}
取消上传是将文件所有请求取消并发送请求删除文件,这里不加赘述。
版权归原作者 藤井粟 所有, 如有侵权,请联系我们删除。