前言
在项目开发的过程中,经常会遇到上传和下载,对于上传来说,如果是小文件的话,接口响应会比较快,但是对于大文件,则需要对其分片以减少请求体的大小和上传时间。
小文件上传
以Vue框架使用<el-upload>
为例,直接上代码
<template><div><el-uploadclass="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;// 创建一个数组来存储每个分片上传的 Promiseconst 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));// 创建分片上传的 Promiseconst 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: '上传失败!',});}},