前言
在项目开发的过程中,经常会遇到上传和下载,对于上传来说,如果是小文件的话,接口响应会比较快,但是对于大文件,则需要对其分片以减少请求体的大小和上传时间。
小文件上传
以Vue框架使用<el-upload>
为例,直接上代码
<template>
<div>
<el-upload
class="upload-demo"
action="your_upload_api_url"
:on-success="handleSuccess"
:before-upload="beforeUpload"
>
<el-button size="small" type="primary">点击上传</el-button>
<div slot="tip" class="el-upload__tip">只能上传jpg/png文件,且不超过500kb</div>
</el-upload>
</div>
</template>
<script>
export default {
methods: {
handleSuccess(response, file) {
// 处理上传成功的逻辑
console.log(response, file);
},
beforeUpload(file) {
// 在上传之前的操作,例如限制文件类型、大小等
const isJPG = file.type === 'image/jpeg' || file.type === 'image/png';
if (!isJPG) {
this.$message.error('只能上传jpg/png文件');
}
const isLt500K = file.size / 1024 < 500;
if (!isLt500K) {
this.$message.error('文件大小不能超过500KB');
}
return isJPG && isLt500K;
},
},
};
</script>
<style scoped>
/* 样式可以根据自己的需求进行调整 */
.upload-demo {
display: flex;
justify-content: center;
align-items: center;
height: 180px;
}
</style>
在上述代码中:
<el-upload>
组件用于处理文件上传,通过 action 属性指定文件上传的接口。
:on-success 属性绑定一个方法,在文件上传成功后触发。
:before-upload 属性绑定一个方法,在文件上传之前触发,可以在该方法中进行一些操作,如限制文件类型和大小。
元素用于触发文件选择。
请注意替换 your_upload_api_url 为实际的文件上传接口。
分片上传
文件过大时就需要进行文件分片上传,文件分片上传是一种将大文件拆分成小块(分片)并分别上传的策略,这样可以更有效地处理大文件上传,避免一次性上传整个文件可能遇到的网络问题和服务器限制。FormData 对象和一些前端框架/库(如 axios)通常与文件分片上传一起使用。
下面是一个简单的实现示例,使用 FormData 和 axios 进行文件分片上传:
<template>
<div>
<input type="file" ref="fileInput" @change="handleFileChange" />
<button @click="startUpload">开始上传</button>
</div>
</template>
<script>
import axios from 'axios';
export default {
data() {
return {
selectedFile: null,
chunkSize: 1024 * 1024, // 每个分片的大小,这里设置为1MB
};
},
methods: {
handleFileChange(event) {
this.selectedFile = event.target.files[0];
},
async startUpload() {
if (!this.selectedFile) {
alert('请选择文件');
return;
}
// 计算总分片数量
const totalChunks = Math.ceil(this.selectedFile.size / this.chunkSize);
// 循环上传分片
for (let chunkIndex = 0; chunkIndex < totalChunks; chunkIndex++) {
const start = chunkIndex * this.chunkSize;
const end = Math.min(start + this.chunkSize, this.selectedFile.size);
const chunk = this.selectedFile.slice(start, end);
const formData = new FormData();
formData.append('file', chunk);
formData.append('chunkIndex', chunkIndex);
formData.append('totalChunks', totalChunks);
try {
await axios.post('your_chunk_upload_api_url', formData);
console.log(`分片 ${chunkIndex + 1} / ${totalChunks} 上传成功`);
} catch (error) {
console.error(`分片 ${chunkIndex + 1} / ${totalChunks} 上传失败`, error);
// 处理上传失败的逻辑,可以选择中止上传或重试
return;
}
}
console.log('文件上传完成');
},
},
};
</script>
上面是一个分片上传的示例,在实际操作时遇到了一些问题
分片上传遇到的问题
问题1:请求体过大,如何处理?
项目中遇到的文件最大约1个GB,此时直接上传,会报请求体过大的报错,经过调试后发现文件最大传输为50MB。由于项目是依赖于平台,属于平台的子项目,因此在前后端联调时,前端通过nginx转发到对应的接口上。在nginx配置里,有50M大小的限制,修改后生效。后台同事在排查后台代码及配置也发现了请求不能过大的限制条件,即ingress中设置了请求的大小,最终两者同时修改后生效
nginx中的配置修改如下:
问题2:前端如何获取上传进度条?
在使用 axios 进行文件上传时,你可以通过配置 onUploadProgress 属性来监听上传进度。onUploadProgress 允许你在上传过程中获取上传进度,并执行相应的操作。
axios.post('your_upload_api_url', formData, {
onUploadProgress: progressEvent => {
const percentCompleted = Math.round((progressEvent.loaded * 100) / progressEvent.total);
console.log(`上传进度: ${percentCompleted}%`);
// 在这里可以更新进度条或执行其他操作
},
})
.then(response => {
// 处理上传成功的逻辑
console.log(response.data);
})
.catch(error => {
// 处理上传失败的逻辑
console.error('上传失败', error);
});
需要注意的是,这里的progressEvent.total
并不一定和文件的大小相等,处理百分比时,尽量使用total而不是file.size。
问题3:串行上传时文件上传没有问题,但是并行上传时,后台取到的文件片组装后错误?
经过定位,发现是在分片时,最后一片会比较小,如果串行上传时,前端一片片按着顺序依次上传,想优化上传速度,使用并行上传时,最后一片通常都会比前面的分片小,导致最后一片先上传,然后发生组装错误。经过测试,可以采取以下思路:1、前端最后一片在前面的分片都上传完毕后,上传最后一片,可使用for循环配合Promise.all处理;2、后端在拿到所有的分片后再开始组装,而不是边上传边组装。
问题4:进度条上传达到100%后,没有立刻返回结果
这是因为后台接收到文件后,可能还有处理的时间,但是文件已经传到了后台,如果后台没有其他逻辑处理,可以直接返回结果,告知用户上传已完成
最后,放上去部分代码
async handleUpload() {
const file = this.formData.file;
// 初始化分片的大小,可以自定义
const chunkSize = 200 * 1024 * 1024;
// 计算分片的数量, 传参时会用到
const chunkCount = Math.ceil(file.size / chunkSize);
// 用于保存每个分片的信息
const chunks = [];
if (file.size > chunkSize) {
// 分割文件为多个分片
for(let i = 0; i < chunkCount; i++) {
const start = i * chunkSize;
const end = Math.min(file.size, (i + 1) * chunkSize);
const chunk = file.raw.slice(start, end);
chunks.push(chunk);
}
} else {
chunks.push(file.raw);
}
try {
this.uploadLoading = true;
// 创建一个数组来存储每个分片上传的 Promise
const uploadPromises = [];
for(let i = 0; i < chunks.length; i++) {
this.loadedSizeArr[i] = 0;
this.totalSizeArr[i] = 0;
}
const fileFlag = getRandomName();
let percentage = 1;
if (chunks.length > 1) {
percentage = (chunks.length-1) / chunks.length;
}
const headers = {
fileFlag,
fileName: file.name,
'Content-Type': 'multipart/form-data',
}
// 遍历并上传每个分片
for(let i = 0; i < Math.max(chunks.length - 1, 1); i++) {
const formData = new FormData();
formData.append('file', chunks[i]);
formData.append('chunkNumber', String(i+1));
formData.append('totalChunks', String(chunkCount));
// 创建分片上传的 Promise
const uploadPromise = util.post('your_upload_api_url', formData, {
headers,
onUploadProgress: (progressEvent) => {
if (progressEvent.lengthComputable) {
this.loadedSizeArr[i] = progressEvent.loaded;
this.totalSizeArr[i] = progressEvent.total;
const loadedSizeTotal = this.loadedSizeArr.reduce((accumulator, currentValue) => accumulator + currentValue, 0);
const totalSizeTotal = this.totalSizeArr.reduce((accumulator, currentValue) => accumulator + currentValue, 0);
this.percent = Math.round(loadedSizeTotal / totalSizeTotal * percentage * 100);
}
},
})
// 将 Promise 存储到数组中
uploadPromises.push(uploadPromise);
}
// 使用 Promise.all 来等待所有分片上传完成
Promise.all(uploadPromises)
.then(async () => {
if (chunkCount > 1) {
const formData = new FormData();
formData.append('file', chunks[chunks.length-1]);
formData.append('chunkNumber', String(chunkCount));
formData.append('totalChunks', String(chunkCount));
await util.post('your_upload_api_url', formData, {
headers,
onUploadProgress: (progressEvent) => {
if (progressEvent.lengthComputable) {
this.loadedSizeArr[chunks.length-1] = progressEvent.loaded;
this.totalSizeArr[chunks.length-1] = progressEvent.total;
const loadedSizeTotal = this.loadedSizeArr.reduce((accumulator, currentValue) => accumulator + currentValue, 0);
const totalSizeTotal = this.totalSizeArr.reduce((accumulator, currentValue) => accumulator + currentValue, 0);
this.percent = Math.round((percentage + loadedSizeTotal / totalSizeTotal / chunkCount) *100);
}
},
})
}
// 所有分片上传完成后执行的逻辑
this.percent = 100;
this.uploadLoading = false;
this.$notify({
type: 'success',
title: '成功',
message: '上传成功!',
})
})
} catch (e) {
this.$notify({
type: 'error',
title: '失败',
message: '上传失败!',
});
}
},