vue3如何实现断点续传

发布时间 2023-12-19 18:35:37作者: Xproer-松鼠

首先创建一个vue3项目

普通上传
// template
<input type="file" ref="uploadRef" @change="upload" />
// js setup
function upload(event) { let files = event.target.files
let formData = new FormData() formData.append("file",files[0]) }
// 如果多文件上传,自己封装队列即可,当然你也可以for上传,开心就好?

 

断点续传(单文件)
正常情况下使用普通上传就行了,但是随着产品的升级优化,产品经理他不同意呀,我们应该整点更贴心用户的操作是不,所以需要一个可以点击暂停的上传的操作。
?于是小弟我又准备在shi山?‍♂️上雕刻了。。
首先我就先整理了一下思路?
如果我想能暂停呢,我就需要把文件分割成许多个小切片来分批上传,这样我就可以暂停啦!

大致想到的流程如下:
获取文件并且分割成许多个切片
单独上传每个切片
获取完整文件(请求接口,后端把切片合并成文件)
代码如下
//App.vue
<template>
<div>
<input type="file" ref="uploadRef" @change="upload" />
<button @click="changeStauts">{{ uploadStatus ? '暂停':'开始' }}--{{ ((1 - requestFn.length / total)*100).toFixed(0) }}%</button>
</div>
</template>
<script setup>
import { reactive, ref } from "@vue/reactivity";
import { uploadChunk,mergeChunk } from "./api.js"
function upload(event) {
total.value = 0
let files = event.target.files
const reader = new FileReader();
reader.onload = () => {
setChunk(reader.result)
total.value = requestFn.length
startUpload()
};
reader.readAsArrayBuffer(files[0]);
}
let fileSize = 1 * 1024 * 1024 // 1M
let requestFn = reactive([])
let total = ref(1)
// 切割分片,并且为每个切片封装一个ajax请求
function setChunk(fileBuffer, i = 0) {
// 确定每个切片的起始位置
let index = i + fileSize
let isEnd = false
if (index > fileBuffer.byteLength) {
index = fileBuffer.byteLength
isEnd = true
}
// 封装ajax请求
requestFn.push(()=>{
return new Promise(async (reslove,reject)=>{
let formData = new FormData()
formData.append("file",fileBuffer.slice(i,index))
const { code } = await uploadChunk(formData) // uploadChunk 为上传接口
if( code == 200 ){
// 如果上传此分片成功,则从数组中删除此方法
requestFn.splice(0,1)
}
reslove()
})
})
// 判断当前文件的切片是否已经全部封装
if (isEnd) {
return
}else{
setChunk(fileBuffer,index)
}
}
const uploadStatus = ref(true)
// 开始上传
async function startUpload(){
await requestFn[0]()
if (requestFn.length > 0) {
if (uploadStatus.value) {
startUpload()
}
}else{
// 上传完毕,请求合并文件//获取合并结果 data
const { data } = await mergeChunk()
}
}
// 暂停或者继续
function changeStauts(){
uploadStatus.value = !uploadStatus.value
if (uploadStatus.value) {
startUpload()
}
}
</script>


大致的断点续传就是这样子的,但是如果只是这样处理的话,后面恐怕还需要继续雕刻这个shi山?。

?那后端在接收到合并文件的时候如何知道需要合并哪些切片呢?
---- 所以这个时候我们需要给上传的文件绑定一个唯一并且不会重复的KEY 上传切片的时候带上KEY
请求合并文件的时候也带上KEY。这样后端就知道合并哪些文件了
---- 一般是用文件hash值来作为KEY的,
但是我们没有做那么复杂的功能,于是就没有使用hash,
这里我使用了用户Id+时间戳,偷了个懒?

...
requestFn.push(()=>{
return new Promise(async (reslove,reject)=>{
let formData = new FormData()
formData.append("file",fileBuffer.slice(i,index))
formData.append("key",userId + new Date().getTime() + '_' + i)
const { code } = await apiUploadFile(formData) // apiUploadFile 为上传接口
if( code == 200 ){
// 如果上传此分片成功,则从数组中删除此方法
requestFn.splice(0,1)
}
reslove()
})
})
...

断点续传(多文件)
相比单个文件,多文件上传这里我们需要一个数组来管理文件上传的进度。

// template app.vue
<template>
<div>
<input type="file" ref="uploadRef" @change="upload" multiple />
<template v-for="item in fileList" :key="item.key">
<br> <button @click="changeStauts(item.key)">{{item.name}}{{ item.status ? '暂停':'开始' }} {{ ((1 - item.requestFn.length / item.total)*100).toFixed(0) }}%</button>
</template>
</div>
</template>
// script App.vue
<script setup>
import { reactive } from 'vue';
import { uploadChunk,mergeChunk } from "./api.js"
// 触发上传 (1)
function upload(){
let files = event.target.files
for (let i = 0; i < files.length; i++) {
setFileBuffer(files[i],i)
}
}
let fileList = reactive([])
// 创建切片相关信息 (2)
function setFileBuffer(file, i) {
const reader = new FileReader();
reader.onload = () => {
let key = new Date().getTime() + `${i}`
fileList.push({ total: 0, status: true, name: file.name, requestFn: [], key: key })
setChunk(reader.result, 0, key)
};
reader.readAsArrayBuffer(file);
}
let fileSize = 1 * 1024 * 1024 // 1M
// 为每个切片封装ajax请求
function setChunk(fileBuffer, i = 0, key) {
let index = i + fileSize
let isEnd = false
if (index > fileBuffer.byteLength) {
index = fileBuffer.byteLength
isEnd = true
}
// 根据key,获取当前文件处于fileList中的下标
const keyIndex = fileList.findIndex(item=>item.key == key)
// 封装每个切片
fileList[keyIndex].requestFn.push(()=>{
return new Promise(async (reslove,reject)=>{
let formData = new FormData()
formData.append("file",fileBuffer.slice(i,index))
formData.append("key",key)
await uploadChunk(formData) // apiUploadFile 为上传接口
fileList[keyIndex].requestFn.splice(0,1)
reslove(true)
})
})
// 判断当前文件的切片是否已经全部封装
if (isEnd) {
fileList[keyIndex].total = fileList[keyIndex].requestFn.length
startUpload(key)
return
}else{
setChunk(fileBuffer, index, key)
}
}

// 开始上传(步骤3)
async function startUpload(key){
const keyIndex = fileList.findIndex(item=>item.key == key)
await fileList[keyIndex].requestFn[0]()
if (fileList[keyIndex].requestFn.length > 0) {
if (fileList[keyIndex].status) {
startUpload(key)
}
}else{
// 上传完毕,请求合并文件
const { data } = await mergeChunk({key:key})
}
}
// 暂停或者继续
function changeStauts(key){
const keyIndex = fileList.findIndex(item=>item.key == key)
fileList[keyIndex].status = !fileList[keyIndex].status
if (fileList[keyIndex].status) {
startUpload(key)
}
}
</script>


断点续传(秒传+多文件)
到这里前面两个案例已经实现了断点续传,如果只是做到这些的话,新的问题也就出现了?。

当用户正在上传的时候,当前窗口被关闭,用户则需要重新上传,这样之前上传的切片就会在服务器的某个角落里吃灰。

当然然我们上传的时候是不会关闭窗口的,但是架不住意外的可能,所以我们还是要优化一下(本想好好的打酱油,但是tm这越做越多…?)

大致思路如下:
获取文件并且分割成许多个切片
上传每一个切片(同时为每个切片生成唯一hash值,文件不变hash不变,文件内容发生改变hash就随之变化,后端会根据这个hash值来判断服务器中是否有当前切片,如果有,就不需要再上传了)
请求合并文件 这里我们需要借助
buffer转hash(md5)工具

// App.vue
import "./spark-md5.min.js"
function setChunk(fileBuffer, i = 0, key) {
...
// 封装每个切片
fileList[keyIndex].requestFn.push(()=>{
return new Promise(async (reslove,reject)=>{
let formData = new FormData()
formData.append("file",fileBuffer.slice(i,index))
formData.append("key",key)
const chunkHash = await setChunkHash(fileBuffer.slice(i,index))
formData.append("hash",chunkHash)
await apiUploadFile(formData) // apiUploadFile 为上传接口
fileList[keyIndex].requestFn.splice(0,1)
reslove(true)
})
})
...
}
// 计算hash值
function setChunkHash(chunk) {
return new Promise((reslove,reject)=>{
const spark = new SparkMD5.ArrayBuffer();
spark.append(chunk);
reslove(spark.end())
})
}


在计算hash值的时候,其实是很消耗时间的。文件比较小倒是无所谓,但是大文件可能造成阻塞。

可采用下面方法来减轻阻塞(二选一或者都要)

webwork 子线程,
requestIdleCallback 浏览器空闲时间(特别装逼,但是我没用它),?javaScript如何使用浏览器空闲时间
好了,最后想说,方法有很多种,具体情况具体分析吧

参考文章:http://blog.ncmem.com/wordpress/2023/12/19/vue3%e5%a6%82%e4%bd%95%e5%ae%9e%e7%8e%b0%e6%96%ad%e7%82%b9%e7%bb%ad%e4%bc%a0/

欢迎入群一起讨论