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

您所在的位置:网站首页 vue-simple-uploader官网 Vue+SpringBoot上传大文件,包括暂停和继续(适用Vue2和Vue3)

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

2024-07-14 23:47| 来源: 网络整理| 查看: 265

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 大文件上传和断点续传

安装 npm install spark-md5 -S 上传文件button

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

将文件拖到此处,或点击上传

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

处理文件状态改变 async handleChange(file) { if (!file) return this.percent = 0 this.percentCount = 0 this.videoUrl = '' // 获取文件并转成 ArrayBuffer 对象 const fileObj = file.raw this.file = fileObj.name let buffer try { buffer = await this.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 = new SparkMD5.ArrayBuffer() spark.append(buffer) const hash = spark.end() // 生成切片,这里后端要求传递的参数为字节数据块(chunk)和每个数据块的文件名(fileName) let curChunk = 0 // 切片时的初始位置 for (let i = 0; i { const fn = () => { const formData = new FormData() formData.append('chunk', item.chunk) formData.append('filename', item.fileName) return axios({ 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 传递给服务器 const complete = () => { 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) } }) } const send = async () => { if (!this.upload) return if (i >= requestList.length) { // 发送完毕 complete() return } await requestList[i]() i++ send() } send() // 发送请求 }, // 将 File 对象转为 ArrayBuffer fileToBuffer(file) { return new Promise((resolve, reject) => { const fr = new FileReader() fr.onload = e => { resolve(e.target.result) } fr.readAsArrayBuffer(file) fr.onerror = () => { reject(new Error('转换文件格式发生错误')) } }) }

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

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

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

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

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

后端

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

上传文件分片 @Value("${file.path}") // 在application.properties中设置了对应的路径 private String dirPath; @PostMapping("/chunk") public ResponseMessage upLoadChunk(@RequestParam("chunk") MultipartFile chunk, @RequestParam("filename") String filename) { // 用于存储文件分片的文件夹 File folder = new File(dirPath); if (!folder.exists() && !folder.isDirectory()) folder.mkdirs(); // 文件分片的路径 String filePath = dirPath + File.separator + filename; try { File saveFile = new File(filePath); // 写入文件中 FileOutputStream fileOutputStream = new FileOutputStream(saveFile); fileOutputStream.write(chunk.getBytes()); fileOutputStream.close(); chunk.transferTo(saveFile); System.out.println(filename); return ResponseMessage.ok(); } catch (Exception e) { e.printStackTrace(); } return ResponseMessage.ok(); } 上传合并文件分片 @GetMapping("/merge") public ResponseMessage MergeChunk(@RequestParam("hash") String hash, @RequestParam("filename") String filename) { // 文件分片所在的文件夹 File chunkFileFolder = new File(dirPath); // 合并后的文件的路径 File mergeFile = new File(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 = new RandomAccessFile(mergeFile, "rw"); byte[] bytes = new byte[1024]; for (File chunk : files) { RandomAccessFile randomAccessFileReader = new RandomAccessFile(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); return ResponseMessage.ok(mergeFile); }

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



【本文地址】


今日新闻


推荐新闻


CopyRight 2018-2019 办公设备维修网 版权所有 豫ICP备15022753号-3