首页 > 其他分享 >vue-大文件分片及断点上传

vue-大文件分片及断点上传

时间:2023-11-15 16:13:13浏览次数:34  
标签:vue const 断点 fileUpIdx filesArr file 分片 上传

最近开发过程中,有一个大文件分片上传的功能,借鉴于网上的思路,结合自己后端的逻辑,完成了这个功能,在此记录如下:

界面展示:

 

 

 

 

一、将大文件分片上传写为一个组件,可以全局注册该组件,也可以在使用的页面注册该组件,使用vuex通讯进行组件间传值

由于我有好几个页面需要使用大文件上传,所以我是在App.vue注册该组件

<template>
<a-config-provider :locale="locale">
<div id="app">
<router-view />
<!-- 将分片上传组件全局注册 -->
<big-uploader></big-uploader>
</div>
</a-config-provider>
</template>
<script>
...
import BigUploader from './components/bigUploader.vue';
export default {
data () {
return {
...
};
},
components: { BigUploader },
...
};
</script>

二、文件选择上传框通过文件上传前的钩子,会有项目的业务逻辑判断,校验文件空间是否还有,判断文件是否为重复文件,提示是覆盖还是重新上传,校验都通过后就去调起大文件上传的组件

beforeUpload(file) {
const self = this
let size = file.size
let params = {
fileSize: size,
projectCode: self.projectCode
}
if (size === 0) {
this.$message.error('上传文件不能为0KB')
return false
}
const fileName = file.name
let fileType = fileName.split('.')
fileType = fileType[fileType.length - 1].toLowerCase()//转小写
if (this.accept.indexOf(fileType) == -1) {
this.$message.error('请上传指定文件类型')
return false;
}
if (this.uploadFlag) return false;
this.uploadFlag = true
return new Promise(() => {
try {
// 一、校验文件空间
request(xxx).then(res => {
if (res.code === 200) {
// 二、计算MD5
self.calculate(file, async function (md5) {
const par = {
fileName: file.name,
md5Value: md5,
parantCode: self.parentCode,
projectCode: self.projectCode
}
// 三、文件覆盖检查
request(xxx).then(res => {
console.log(res)
if (res.code === 200 && res.data) {
//同名同类型提示是否覆盖
self.$confirm({
title: false,
content: '该文件夹下存在同名同类型的文件,是否确认覆盖?',
onOk() {
// 覆盖
self.implementSetFile(file, md5)
},
onCancel() {
self.handelModelCancal();
},
});
} else {
// 新文件
self.implementSetFile(file, md5)
}
return false
})
})
}
}).catch(() => {
self.uploadFlag = false
return false;
})
}
catch {
self.uploadFlag = false
return false;
}
})
},
implementSetFile(file, md5) {
// 设置当前上传文件
this.setFile({
page: this.curMenuName.name,
bucket: 'privately',
projectCode: this.projectCode,
parentCode: this.parentCode,
record: this.record,
file: file,
md5: md5
})
// 设置显示上传框
this.setShowBigUpload(true)
// this.$emit('ok');
this.uploadFlag = false
this.$emit('closeUpload')
},
/**
* 计算md5,实现断点续传及秒传
* @param file
*/
calculate(file, callBack) {
var fileReader = new FileReader(),
blobSlice = File.prototype.mozSlice || File.prototype.webkitSlice || File.prototype.slice,
chunkSize = 2097152,
// read in chunks of 2MB
chunks = Math.ceil(file.size / chunkSize),
currentChunk = 0,
spark = new SparkMD5();

fileReader.onload = function (e) {
spark.appendBinary(e.target.result); // append binary string
currentChunk++;

if (currentChunk < chunks) {
loadNext();
}
else {
callBack(spark.end());
}
};

function loadNext() {
var start = currentChunk * chunkSize,
end = start + chunkSize >= file.size ? file.size : start + chunkSize;

fileReader.readAsBinaryString(blobSlice.call(file, start, end));
}

loadNext();
},


三、大文件组件

data中定义当前上传下标,上传并发数,允许每个分片最多允许重传数,上传文件列队数组等值

通过mapGetters获取vuex值


...mapGetters({
showUploadBox: `${NAMESPACE_UPLOAD}/${GET_SHOW_BIG_UPLOAD}`,
file: `${NAMESPACE_UPLOAD}/${GET_FILE}`,
chunkSize: `${NAMESPACE_UPLOAD}/${GET_CHUNKSIZE}`,
threads: `${NAMESPACE_UPLOAD}/${GET_THREADS}`,
getUplodRes: `${NAMESPACE_UPLOAD}/${GET_UPLOAD_RES}`,
}),

通过监听file变化,触发大文件上传排队



watch: {
file: {
immediate: true,
handler: function (newVal) {
if (newVal) {
const tempMd5 = newVal.md5
// 判断文件是否在排队中
if (this.upFileObj[tempMd5]) {
this.$message.warn('当前文件已经在上传排队中,请不要重复选择')
} else {
newVal.fileIndex = this.uploadFiles.length
this.upFileObj[tempMd5] = newVal
this.uploadQueue(tempMd5)

}

}
}
}
}

通过uploadQueue组件上传排队当前文件的分片,通过md5校验资源是否已经存在,记录资源是否存在和是否需要上传



/*
* 上传排队
* 计算当前文件的Md5和分片
*/
uploadQueue(md5) {
console.log('1.当前文件上传排队')
const self = this;
const tempFile = self.upFileObj[md5]
const fileName = tempFile.file.name
const curFileSize = tempFile.file.size
self.handleInitRawFile(tempFile);
// 检验资源是否存在
request(xxx).then(res => {
if (res.code === 200) {
var resData = res.data
const fileIndex = tempFile.fileIndex
const filesArr = self.uploadFiles;
if (resData) {
// 资源已经存在
console.log('3.上传文件已经存在', resData)
filesArr[fileIndex].Already = true
filesArr[fileIndex].resData = resData
self.$set(filesArr, fileIndex, filesArr[fileIndex]);
} else {
// 需要上传
let needChunk = false
// 不需要分片
if (curFileSize < self.chunkSize) {
needChunk = false
} else {
needChunk = true
}
filesArr[fileIndex].needChunk = needChunk
self.$set(filesArr, fileIndex, filesArr[fileIndex]);
}
// 判断是否正在上传中
if (self.uploadLock) {
console.log('有文件正在上传中...')
} else {
self.beforeUpload()
}
}
})
},
/*
** 初始化部分自定义上传属性
*/
handleInitRawFile(rawFile) {
console.log('2.初始化部分自定义上传属性')
rawFile.status = fileStatus.md5;
rawFile.initFail = this.file
rawFile.chunkList = [];
rawFile.uploadProgress = 0;
rawFile.fakeUploadProgress = 0; // 假进度条,处理恢复上传后,进度条后移的问题
rawFile.hashProgress = 0;
this.uploadFiles.push(rawFile);
},

文件正式上传前,判断资源是否存在直接秒传,是否需要上传,上传的话是否需要进行分片,秒传和不需要分片都是简单的业务逻辑,下面展示一下主要的分片代码

需要分片,组件分片上传数组



/*
** 开始组建上传数组
*/
async handleUpload() {
console.log('6.开始组建上传数组')
if (!this.uploadLock) return;
const filesArr = this.uploadFiles;
const fileUpIdx = this.fileUpIdx
const fileChunkList = this.createFileChunk(filesArr[fileUpIdx].file);
if (filesArr[fileUpIdx].status !== 'resume') {
this.status = Status.hash;
// hash校验,是否为秒传
filesArr[fileUpIdx].hash = await this.calculateHash(fileChunkList);
// 若清空或者状态为等待,则跳出循环
if (this.status === Status.wait) {
console.log('若清空或者状态为等待,则跳出循环');
return
}
}

this.status = Status.uploading;
filesArr[fileUpIdx].status = fileStatus.uploading;
filesArr[fileUpIdx].fileHash = filesArr[fileUpIdx].hash; // 文件的hash,合并时使用
filesArr[fileUpIdx].chunkList = fileChunkList.map(({ file }, index) => ({
fileHash: filesArr[fileUpIdx].hash,
fileName: filesArr[fileUpIdx].file.name,
index,
hash: filesArr[fileUpIdx].hash + '-' + index,
chunk: file,
size: file.size,
uploaded: false, // 标识:是否已完成上传
progress: 0,
status: 'wait' // 上传状态,用作进度状态显示
}));
this.$set(filesArr, fileUpIdx, filesArr[fileUpIdx]);
...
},

分片-初始化任务的时候,将分片数组及文件md5传给后端,后端返回该文件上传的任务id,及没有上传的片段数组及小片段id,已经上传的片段,实现秒传效果,这里就是断点续传的主要逻辑思路


initJob() {
console.log('7.分片-初始化任务')
const filesArr = this.uploadFiles;
const fileUpIdx = this.fileUpIdx
const uploadFileMd5 = filesArr[fileUpIdx].md5
let detailList = []
filesArr[fileUpIdx].chunkList.forEach((item, idx) => {
detailList.push({
extInfo: '',
// file: item.chunk,
num: idx
})
})
const params = {
md5HashValue: uploadFileMd5,
detailList: detailList
}
request(xxx).then(res => {
console.log('分片-初始化任务', res)
if (res.code === 200) {
const resData = res.data
if (resData.jobStatus === 1) {
console.log('上传文件已经存在', resData)
} else {
let sliceList = []
this.jobCode = resData.jobCode
resData.sliceList.forEach(item => {
const tt = {
...item,
...filesArr[fileUpIdx].chunkList[item.num]
}
sliceList.push(tt)
})
// this.sliceList = sliceList
filesArr[fileUpIdx].chunkList = sliceList
this.$set(filesArr, fileUpIdx, filesArr[fileUpIdx]);
//没有上传的分片
let noUpSlice = [], yesUpSlice = [];
sliceList.filter((item, idx) => {
item.num = idx
if (item.uploadStatus === 0) {
noUpSlice.push({ ...item });
} else {
yesUpSlice.push({ ...item });
}
})
// const noUpSlice = sliceList.filter(({ uploadStatus }) => uploadStatus === 0)
console.log('没有上传的分片', noUpSlice)
if (noUpSlice.length === 0) {
this.mergeRequest();
} else {
if (yesUpSlice.length > 0) {
yesUpSlice.forEach(item => {
this.createProgresshandler(100, item.num)
})
}
this.uploadChunks(noUpSlice, sliceList);
}
}
}
})
},

将切片传输给服务端,进行并发上传处理,所有的分片上传完成,向服务端进行合并请求


async uploadChunks(data, allData) {
console.log('8.将切片传输给服务端')
return new Promise(async (resolve, reject) => {
const requestDataList = data.map(({ sliceCode, num, chunk }) => {
const formData = new FormData();
formData.append('sliceCode', sliceCode);
formData.append('file', chunk);
return { formData, num };
})
try {
const ret = this.sendRequest(requestDataList, data, allData);
} catch (error) {
// 上传有被reject的
this.$message.error('亲 上传失败了,考虑重试下呦' + error);
return;
}
// 合并切片
const isUpload = data.some((item) => item.uploadStatus === 0);
if (!isUpload) {
// 执行合并
try {
await this.mergeRequest();
resolve();
} catch (error) {
reject();
}
}
});
},
sendRequest(forms, chunkData, allData) {
console.log('9.分片-并发上传处理')
var finished = 0;
const total = forms.length;
const that = this;
const retryArr = []; // 数组存储每个文件hash请求的重试次数,做累加 比如[1,0,2],就是第0个文件切片报错1次,第2个报错2次
// const md5Hash = allData[0].fileHash
const filesArr = this.uploadFiles;
const fileUpIdx = this.fileUpIdx
const md5Hash = filesArr[fileUpIdx].md5

return new Promise((resolve, reject) => {
const handler = () => {
if (forms.length) {
// 出栈
const formInfo = forms.shift();
const formData = formInfo.formData;
const index = formInfo.num;
/*
*** 开始分片上传
*/
// console.log('当前分片上传', allData[index])
if (allData[index]) {
request(xxx)
.then(res => {
// 更改状态
allData[index].uploaded = true;
allData[index].uploadStatus = 1;
allData[index].status = 'success';
finished++;
if (finished === chunkData.length) {
// 执行合并
this.mergeRequest();
}
handler();
}).catch((e) => {
// 若状态为暂停或等待,则禁止重试
if ([Status.pause, Status.wait].includes(this.status)) return;
console.warn('出现错误', e);
console.log('当前分片上传报错', retryArr);
if (typeof retryArr[index] !== 'number') {
retryArr[index] = 0;
}
// 更新状态
allData[index].status = 'warning';
// 累加错误次数
retryArr[index]++;
// 重试3次
if (retryArr[index] >= this.chunkRetry) {
console.warn(' 重试失败--- > handler -> retryArr', retryArr, allData[index].hash);
return reject('重试失败', retryArr);
}
// console.log('handler -> retryArr[finished]', `${allData[index].hash}--进行第 ${retryArr[index]} '次重试'`);
// console.log(retryArr);

this.tempThreads++; // 释放当前占用的通道
// 将失败的重新加入队列
forms.push(formInfo);
handler();
})
}
}
if (finished >= total) {
resolve('done');
}
};
// 控制并发
for (let i = 0; i < this.tempThreads; i++) {
handler();
}
});
},

通知服务端合并切片,设置总的文件进度,并设置上传结果,回传给页面展示新增文件,进行下一步业务操作

在此大文件上传的整个思路就完成了。

 

参考文章:http://blog.ncmem.com/wordpress/2023/11/15/vue-%e5%a4%a7%e6%96%87%e4%bb%b6%e5%88%86%e7%89%87%e5%8f%8a%e6%96%ad%e7%82%b9%e4%b8%8a%e4%bc%a0/

欢迎入群一起讨论

 

 

标签:vue,const,断点,fileUpIdx,filesArr,file,分片,上传
From: https://www.cnblogs.com/songsu/p/17834084.html

相关文章

  • Vue轻量级富文本编辑器-Vue-Quill-Editor
    先看效果图:女神镇楼1.下载Vue-Quill-Editornpminstallvue-quill-editor--save2.下载quill(Vue-Quill-Editor需要依赖)npminstallquill--save3.代码<template><divclass="edit_container"><quill-editorv-model="cont......
  • Vue 2.x脱坑记-查漏补缺
    Q:组件的通讯有哪几种啊!基本最常用的是这三种;父传子: props子传父: emit兄弟通讯:eventbus:就是找一个中间组件来作为信息传递中介vuex:信息树传送门:基本通讯VuexQ:为什么我的 npm 或者 yarn 安装依赖会生成 lock文件,有什么用!lock文件的作用......
  • vue~封装一个文本框标签组件
    用到的技术父组件向子组件的传值类型检查和默认值:您可以为props指定类型检查和默认值。这可以确保传递给子组件的数据符合期望的类型,以及在没有传递数据时具有合理的默认值。例如:props:{message:{type:String,default:'DefaultMessage'},count:{typ......
  • vue~封装一个文本框添加与删除的组件
    标签组件的效果如下组件作用这是一个div,包含了两个文本框,后面是添加和删除按钮添加按钮复制出新的div,除了文本框没有内容,其它都上面一样删除按钮将当前行div删除组件实现<template><div><templatev-for="(item,index)intags"><el-row:gutter="4"style="margin:......
  • Vue双向数据绑定原理-上
    Vue响应式的原理(数据改变界面就会改变)是什么?时时监听数据变化,一旦数据发生变化就更新界面,这就是Vue响应式的原理。Vue是如何实现时时监听数据变化的通过原生JS的defineProperty方法,通过get和set方法来监听数据的变化。defineProperty方法的特点可以直接在一个对象上定义一......
  • Vue实验记录
    webpack安装首先下载node.jshttps://nodejs.org/en下载完成后进行安装,直接下一步就可以安装完成后,在cmd中查看是否安装成功然后安装webpack安装脚手架创建项目选择第二个创建完成后的效果进入项目并运行在学习通中下载源码,把源码按照项目格式放到创建好的项目......
  • vuejs3.0 从入门到精通——项目搭建
    项目搭建一、环境准备软件名称软件版本nodev20.9.0npm10.1.0Windows10专业版22H2vue/cli5.0.8vitev4.5.0二、vite创建项目>npminitvite@latestsaas--templatevue√Selectaframework:»Vue√Selectavariant:»TypeScriptSca......
  • Vue-cli 用自定义的组件有遇到过哪些问题?
    在components目录新建你的组件文件(indexPage.vue),script一定要exportdefault{}在需要用的页面(组件)中导入:importindexPagefrom'@/components/indexPage.vue'注入到vue的子组件的components属性上面,components:在template视图view中使用,例如命名为inde......
  • Vue3+Element plus 实现表格可编辑
    <template><div><el-buttontype="primary"@click="handleAdd">新增</el-button></div><div><el-table:data="tableData"style="width:100%&quo......
  • vue3 与 vue2 的区别
    布尔型Attribute​布尔型attribute依据true/false值来决定attribute是否应该存在于该元素上。disabled就是最常见的例子之一。v-bind在这种场景下的行为略有不同:<button:disabled="isButtonDisabled">Button</button>当isButtonDisabled为真值或一个空字符串(......