Vue3+Ts的大文件分片上传,秒传,断点续传
1.为什么要使用分片上传
分片上传(Chunked Upload)是一种将大文件分割成多个较小的部分进行上传的技术。这种技术有几个优点:
稳定性: 上传大文件时,如果整个文件一次性上传,可能会因为网络不稳定或其他原因导致上传中断。使用分片上传可以降低上传失败的风险,因为只需要重新上传失败的部分,而不是整个文件。
进度显示: 对于大文件的一次性上传,用户可能会感觉上传过程很慢,因为他们无法看到上传的进度。而使用分片上传,可以在每个分片上传完成后更新上传进度,让用户更清楚地了解上传的进度。
并行处理: 分片上传可以让客户端同时上传多个分片,从而提高上传效率。这对于大文件上传来说尤为重要,因为这样可以充分利用网络带宽,加快上传速度。
断点续传: 如果上传过程中断,用户可以从已经上传完成的分片继续上传,而不需要重新上传整个文件。这样可以节省时间和带宽,并提升用户体验。
总的来说,分片上传可以提高大文件上传的稳定性、效率和用户体验,因此在需要上传大文件的应用场景中,使用分片上传是一种常见的做法。
下面我们来开始开发此项功能:实现大文件分片上传,秒传,断点续传,进度管控
1.技术栈
所用技术为Vue3+TS,UI框架使用arco-design,接口请求使用fetch
2.构建页面:构建上传组件,以及进度条控制
<template>
<div>
<a-upload :auto-upload="false" ref="uploadRef" @change="fileChange" multiple draggable>
</a-upload>
<a-progress
:percent="percentage"
:style="{ width: '50%' }"
:color="{
'0%': 'rgb(var(--primary-6))',
'100%': 'rgb(var(--success-6))',
}"
/>
<a-button type="primary" @click="submit"> start upload</a-button>
</div>
</template>
3.获取文件进行分片,并获取文件唯一的hash值
import { ref } from "vue";
import SparkMD5 from "spark-md5";
import { Message } from "@arco-design/web-vue";
const uploadRef = ref()
const percentage = ref()
let chunkSize = 1024 * 512
// 存放所有分片内容
let chunkRes = ref()
let fileName = ref('')
let fileHash = ref('')
const fileChange = async (value: any) => {
// 获取文件
const files = value[0].file
if (!files) {
return
}
fileName.value = files.name
chunkRes.value = createFileChunk(files) //分片
const res = await getUniqueHash(chunkRes.value) //生成hash值
fileHash.value = res as string
}
// 执行文件分片操作
const createFileChunk = (file: File) => {
const chunkList = [];
let cur = 0;
while (cur < file.size) {
chunkList.push(file.slice(cur, cur + chunkSize));
cur += chunkSize;
}
return chunkList;
};
// 使用hash识别文件,实现秒传的功能
const getUniqueHash = (fileChunks: any) => {
return new Promise(resolve => {
const spark = new SparkMD5.ArrayBuffer()
const fileReader = new FileReader()
const target: Blob[] = []
fileChunks.forEach((item: Blob, index: number) => {
if (index === 0 || index === fileChunks.length - 1) {
target.push(item)
} else {
// 当前切片的前面2字节
target.push(item.slice(0, 2))
// 中间的两个字节
target.push(item.slice(chunkSize / 2, chunkSize / 2 + 2))
// 后面的两个字节
target.push(item.slice(chunkSize - 2, chunkSize))
}
})
fileReader.readAsArrayBuffer(new Blob(target)) //将target中的Blob对象转化为ArrayBuffer
fileReader.onload = (e: any) => { //onload方法是异步的
spark.append(e.target?.result) //SparkMD5 库的 append 方法将读取的 ArrayBuffer 添加到 MD5 计算中
resolve(spark.end())
}
})
}
4.上传分片,并识别当前的分片是否已经上传实现断点续传
// 上传分片 chunkRes是所有分片 data.existsChunks是已经存在的分片
const uploadChunk = async (chunkRes: any, existsChunks:string[]) => {
// 处理数据成我们需要的格式
const data = chunkRes.map((item: Blob, index: number) => {
return {
fileHash: fileHash.value,
chunkHash: `${fileHash.value}-${index}`,
chunk: item,
size: item.size,
}
})
// 添加为formData对象
const formDatas = data
.filter((item:any) => !existsChunks.includes(item.chunkHash))
.map((item: any) => {
const formData = new FormData()
formData.append('chunk', item.chunk)
formData.append('chunkHash', item.chunkHash)
formData.append('fileName', fileName.value)
formData.append('fileHash', fileHash.value)
return formData
})
// 控制它的最大请求书
let max = 1
let index = 0 //文件是否上传完毕控制
const taskPool: any = [] //文件控制池
while (index < formDatas.length) {
const task = fetch('http://localhost:3000/upload', {
method: 'POST',
body: formDatas[index]
})
// 当任务执行完成之后,删除taskPool内已完成的数据
task.then(() => {
taskPool.splice(taskPool.findIndex((item: any) => item === task))
})
taskPool.push(task)
// 当数组中的数据为6的时候,循环稍等,等其中一个完成再继续
if (taskPool.length === max) {
await Promise.race(taskPool)
}
console.log(index,'indx')
console.log(formDatas.length,'formDatas.length')
console.log(percentage.value)
index++
percentage.value = (index / formDatas.length).toFixed(2)
}
// 保险起见,将其中的任务再执行一遍
await Promise.all(taskPool)
// 所有的文件都上传完毕了,实现合并请求
mergeFile()
}
5.提交请求,实现秒传以及文件合并
const mergeFile = async () => {
const res = await fetch('http://localhost:3000/merge', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
fileName: fileName.value,
fileHash: fileHash.value,
size: chunkSize
})
})
if (res.status === 200) {
percentage.value = 1
Message.success({
content: '文件上传成功'
})
}
}
// 秒传
const secondPass = async () => {
const res = await fetch('http://127.0.0.1:3000/verify', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
fileName: fileName.value,
fileHash: fileHash.value
})
})
const data = await res.json()
return data
}
const submit = async () => {
const { data } = await secondPass()
if (!data.shouldUpload) {
Message.success({
content: '文件秒传成功'
})
return
}
uploadChunk(chunkRes.value,data.existsChunks)
}