实现的效果:
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); }
取消上传是将文件所有请求取消并发送请求删除文件,这里不加赘述。