vue3如何实现断点续传

首先创建一个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/
欢迎入群一起讨论

在这里插入图片描述

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值