Vue+SpringBoot上传大文件,包括暂停和继续(适用Vue2和Vue3)
Vue+SpringBoot上传大文件,包括暂停和继续,以及进度条process的展示(适用Vue2和Vue3)
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
action="#"
multiple
:auto-upload="false"
:show-file-list="true"
:on-change="handleChange"
drag
>
<!-- 这个图标的书写方式,element-plus和element有区别,注意一下! -->
<el-icon class="el-icon--upload"><upload-filled /></el-icon>
<div class="el-upload__text">
将文件拖到此处,或<em>点击上传</em>
</div>
</el-upload>
因为是自定义上传,所以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 < 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) => {
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
大小。
<!-- 进度显示 -->
<span>上传进度:{{ percent.toFixed() }}%</span>
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;
}
暂停和继续
<!-- 进度显示 -->
<el-button type="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中设置了对应的路径
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
更多推荐
所有评论(0)