Vue+SpringBoot上传大文件,包括暂停和继续(适用Vue2和Vue3)

2022年6月9日12:08:29

Vue+SpringBoot上传大文件,包括暂停和继续(适用Vue2和Vue3)

最近debug发现毕设中有一个上传文件的地方,每当我上传大文件时都上传不上去,最后发现是因为大文件上传服务器太慢,超过了axios限制的50s,所以需要将大文件切片上传。

前端:vue3 +element-plus
后端:springboot

解决思路:

1. 前端在上传文件时将大文件切片向后端发送请求
2. 后端将这些文件切片存储
3. 前端将这些文件切片全部上传完成后,向后端发送merge请求
4. 后端收到merge请求后合并分片

一开始,我查资料发现有个组件vue-simple-uploader,是一个基于simple-uploader.js的Vue上传组件,支持可暂停、继续上传、错误处理、支持“快传”、支持最大并发上传分块上传支持进度、预估剩余时间、出错自动重试、重传等操作,非常符合我的需求。

但是,我按照文档试了半天,一直报错,最后才发现这个组件只能在vue2的环境中使用(我的前端框架是vue3)
如果你是vue2的环境,推荐使用这个组件:
vue-simple-uploader文档
simple-uploader.js文档
vue-simple-uploader常见问题整理

前端

前端主要参考Vue 大文件上传和断点续传

安装
npminstall spark-md5 -S
上传文件button

在vue页面中使用element-ui的上传组件

<el-uploadaction="#"multiple:auto-upload="false":show-file-list="true":on-change="handleChange"drag><!-- 这个图标的书写方式,element-plus和element有区别,注意一下! --><el-iconclass="el-icon--upload"><upload-filled/></el-icon><divclass="el-upload__text">
    将文件拖到此处,或<em>点击上传</em></div></el-upload>

因为是自定义上传,所以el-upload组件的auto-upload要设定为false
show-file-list表示显示已上传文件列表
on-change文件状态改变时的钩子函数,添加文件、上传成功和上传失败时都会被调用

处理文件状态改变
asynchandleChange(file){if(!file)returnthis.percent=0this.percentCount=0this.videoUrl=''// 获取文件并转成 ArrayBuffer 对象const fileObj= file.rawthis.file= fileObj.namelet buffertry{
    buffer=awaitthis.fileToBuffer(fileObj)}catch(e){
    console.log(e)}// 将文件按固定大小(2M)进行切片,注意此处同时声明了多个常量const chunkSize=2097152,
      chunkList=[],// 保存所有切片的数组
      chunkListLength= Math.ceil(fileObj.size/ chunkSize),// 计算总共多个切片
      suffix=/\.([0-9A-z]+)$/.exec(fileObj.name)[1]// 文件后缀名// 根据文件内容生成 hash 值const spark=newSparkMD5.ArrayBuffer()
  spark.append(buffer)const hash= spark.end()// 生成切片,这里后端要求传递的参数为字节数据块(chunk)和每个数据块的文件名(fileName)let curChunk=0// 切片时的初始位置for(let i=0; i< chunkListLength; i++){const item={
       chunk: fileObj.slice(curChunk, curChunk+ chunkSize),
       fileName:`${hash}_${i}.${suffix}`// 文件名规则按照 hash_1.jpg 命名}
     curChunk+= chunkSize
     chunkList.push(item)}this.chunkList= chunkList// sendRequest 要用到this.hash= hash// sendRequest 要用到this.sendRequest()},// 发送请求sendRequest(){const requestList=[]// 请求集合this.chunkList.forEach((item, index)=>{constfn=()=>{const formData=newFormData()
       formData.append('chunk', item.chunk)
       formData.append('filename', item.fileName)returnaxios({
         url:'/backend-api/chunk',
         method:'post',
         headers:{'Content-Type':'multipart/form-data'},
         data: formData}).then(res=>{if(res.data.code===200){// 成功if(this.percentCount===0){// 避免上传成功后会删除切片改变 chunkList 的长度影响到 percentCount 的值this.percentCount=100/this.chunkList.length}if(this.percent>=100){this.percent=100;}else{this.percent+=this.percentCount// 改变进度}if(this.percent>=100){this.percent=100;}this.chunkList.splice(index,1)// 一旦上传成功就删除这一个 chunk,方便断点续传}})}
     requestList.push(fn)})let i=0// 记录发送的请求个数// 文件切片全部发送完毕后,需要请求 '/merge' 接口,把文件的 hash 传递给服务器constcomplete=()=>{axios({
       url:'/backend-api/merge',
       method:'get',
       params:{ hash:this.hash, filename:this.file}}).then(res=>{if(res.data.code===200){// 请求发送成功// this.videoUrl = res.data.path
         console.log(res.data)}})}constsend=async()=>{if(!this.upload)returnif(i>= requestList.length){// 发送完毕complete()return}await requestList[i]()
     i++send()}send()// 发送请求},// 将 File 对象转为 ArrayBufferfileToBuffer(file){returnnewPromise((resolve, reject)=>{const fr=newFileReader()
    fr.onload=e=>{resolve(e.target.result)}
    fr.readAsArrayBuffer(file)
    fr.onerror=()=>{reject(newError('转换文件格式发生错误'))}})}

handleChange(file)函数中,当上传文件后,将文件转成ArrayBuffer对象,并且按照一定的大小切片(此处是2M),同时将这些切片文件分别命名为文件名规则按照hash值_id.文件后缀名命名,例如:f889c389ef0afe9a58ec0afcf92e23d1_0.tar。然后将这些文件切片放在chunkList中,以便后续的分片上传。

sendRequest()中按照chunkList,将这些文件切片放在一个请求集合requestList中,一次向后端发送上传的请求。当文件分片全部上传完成后,发送文件合并请求。

需要注意的是:当文件分片在上传时,我们还需要增加进度条percent,每次都增加percentCount大小。

<!-- 进度显示 --><span>上传进度:{{ percent.toFixed() }}%</span>
if(this.percentCount===0){// 避免上传成功后会删除切片改变 chunkList 的长度影响到 percentCount 的值this.percentCount=100/this.chunkList.length}// 在this.percent+=this.percentCount的前后都判断一次this.percent是否大于等于100if(this.percent>=100){this.percent=100;}else{this.percent+=this.percentCount// 改变进度}if(this.percent>=100){this.percent=100;}
暂停和继续
<!-- 进度显示 --><el-buttontype="primary"size="small"@click="handleClickBtn">{{ upload ? '暂停' : '继续'}}</el-button>
// 按下暂停按钮handleClickBtn(){this.upload=!this.upload// 如果不暂停则继续上传if(this.upload)this.sendRequest()},

前端完整代码可以查看我的代码仓库project-file-upload-frontend

后端

在controller中主要有两个方法(由于是自己写的一个小demo,就没有用到数据库、service、mapper等等,大家可以按需自行添加~)

上传文件分片
@Value("${file.path}")// 在application.properties中设置了对应的路径privateString dirPath;@PostMapping("/chunk")publicResponseMessageupLoadChunk(@RequestParam("chunk")MultipartFile chunk,@RequestParam("filename")String filename){// 用于存储文件分片的文件夹File folder=newFile(dirPath);if(!folder.exists()&&!folder.isDirectory())
        folder.mkdirs();// 文件分片的路径String filePath= dirPath+File.separator+ filename;try{File saveFile=newFile(filePath);// 写入文件中FileOutputStream fileOutputStream=newFileOutputStream(saveFile);
        fileOutputStream.write(chunk.getBytes());
        fileOutputStream.close();
        chunk.transferTo(saveFile);System.out.println(filename);returnResponseMessage.ok();}catch(Exception e){
        e.printStackTrace();}returnResponseMessage.ok();}
上传合并文件分片
@GetMapping("/merge")publicResponseMessageMergeChunk(@RequestParam("hash")String hash,@RequestParam("filename")String filename){// 文件分片所在的文件夹File chunkFileFolder=newFile(dirPath);// 合并后的文件的路径File mergeFile=newFile(dirPath+File.separator+ filename);// 得到文件分片所在的文件夹下的所有文件File[] chunks= chunkFileFolder.listFiles();assert chunks!=null;// 按照hash值过滤出对应的文件分片// 排序File[] files=Arrays.stream(chunks).filter(file-> file.getName().startsWith(hash))// 分片文件命名为"hash值_id.文件后缀名"// 按照id值排序.sorted(Comparator.comparing(o->Integer.valueOf(o.getName().split("\\.")[0].split("_")[1]))).toArray(File[]::new);try{// 合并文件RandomAccessFile randomAccessFileWriter=newRandomAccessFile(mergeFile,"rw");byte[] bytes=newbyte[1024];for(File chunk: files){RandomAccessFile randomAccessFileReader=newRandomAccessFile(chunk,"r");int len;while((len= randomAccessFileReader.read(bytes))!=-1){
                randomAccessFileWriter.write(bytes,0, len);}
            randomAccessFileReader.close();}
        randomAccessFileWriter.close();}catch(Exception e){
        e.printStackTrace();}System.out.println(hash);returnResponseMessage.ok(mergeFile);}

后端完整代码可以查看我的代码仓库project-file-upload-backend

  • 作者:全园最晚睡第一名
  • 原文链接:https://blog.csdn.net/weixin_43977534/article/details/124184369
    更新时间:2022年6月9日12:08:29 ,共 5938 字。