前端大文件分片上传 进度条展示 上传暂停、开始、取消

藤井粟 2024-07-15 15:33:01 阅读 55

在这里插入图片描述

实现的效果:

1、多个大文件(支持10个G以上)分片上传

2、进度条展示进度

3、控制文件上传暂停和取消

实现关键点:

1、文件预处理(md5计算、请求和进度处理等)

2、分片上传的流程(查询已上传分片、文件合并等)

3、文件的暂停、开始、取消

文件预处理

首先使用file类型的input框获取文件,对文件进行深拷贝,再清空input的value值(防止input的change事件不被触发)。

<code>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"code>

v-for="(f, index) in bigFileList"code>

:key="index"code>

>

<div class="bigFileTop">code>

<Tooltip :content="f.file.name" placement="top">code>

<div class="bigFileName">code>

{ { f.file.name }}

</div>

</Tooltip>

<div class="bigFileSize" style="width: 20%">code>

{ { f.newSize }}

</div>

<!-- <div class="bigFileUploadProgress" style="width: 5%">code>

{ { f.progress }}%

</div> -->

<div

class="bigFileStatus"code>

:style="{

width: '20%',

color: f.status == '上传失败' ? 'red' : 'black',

}"code>

>

{ { f.status }}

</div>

<div

class="bigFileActions"code>

style="position: relative; width: 20%"code>

>

<Button

@click="pauseBigFile(f)"code>

type="primary"code>

size="small"code>

style="position: absolute"code>

v-if="!f.isPaused"code>

:disabled="f.progress == 100"code>

>

暂停

</Button>

<Button

@click="restartBigFile(f)"code>

type="primary"code>

size="small"code>

:disabled="f.progress == 100"code>

:style="{ opacity: f.progress == 100 ? 0 : 1 }"code>

>

开始

</Button>

<Button

@click="cancelUpload(f)"code>

type="error"code>

size="small"code>

style="margin-left: 10px"code>

>

取消

</Button>

</div>

</div>

<div class="progress">code>

<Progress :percent="f.progress" :stroke-width="5"></Progress>code>

</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);

}

取消上传是将文件所有请求取消并发送请求删除文件,这里不加赘述。



声明

本文内容仅代表作者观点,或转载于其他网站,本站不以此文作为商业用途
如有涉及侵权,请联系本站进行删除
转载本站原创文章,请注明来源及作者。