之前被问到这个问题,感觉有点搞头。所以抽时间写了个demo,1个file标签2个接口,后端用golang实现
问题抛出
我们都知道,使用<input type="file">
会得到一个文件对象,接着用FormData格式包裹,就可以往后端上传文件了。这种方式一般没问题,但如果文件较大(200M往上),考虑到网洛、性能等因素,就不太合适了。
解决思路
前端–大文件拆分
File对象里有个size属性,表示选择文件所占大小,它的单位是byte。其实看到这,大概想到了方法。前端可以把大文件拆分成几个文件块(这里我设置的 最大2M),一次次的上传,也叫文件流式上传。
//具体实现
const fileName = (this.fileName = file.name);
const size = file.size;
const shardSize = 1024 * 1024 * 2; //2M
const shardCount = Math.ceil(size / shardSize);
// 切割成多个片段
let shardList = [];
for (let i = 0; i < shardCount; i++) {
let formData = new FormData();
formData.append("fileName", fileName);
formData.append("clipNum", i); //第几块
const start = i * shardSize;
const end = Math.min(size, start + shardSize);
formData.append("content", file.slice(start, end)); //用slice方法切片
shardList.push(formData);
}
前端–分批请求
以上 使用File对象自带的slice方法,把二进制文件编码切割成了多个文件块。然后需要发很多请求,一次次的搬运到后端去组装文件,这里请求的部分也要稍微注意下。
假定一个文件200M,然后把它拆分成多个大小为2M的文件块,一次请求处理1个文件块,所以至少也要请求100次。如果同时发送大量请求,对后端并发是个考验,前端的用户体验也不太友好。
这里我写了个请求封装,实现了最大请求并行数,上传错误处理等机制
<template>
<div style="margin: 50px">
<el-upload
class="avatar-uploader"
action=""
:show-file-list="false"
:before-upload="beforeAvatarUpload"
>
<i class="el-icon-plus avatar-uploader-icon"></i>
</el-upload>
</div>
</template>
<script>
import axios from "axios";
axios.defaults.baseURL = "http://localhost:8000";
export default {
data() {
return {
fileName: "",
shardList: [],
mergeApi: false,
};
},
methods: {
beforeAvatarUpload(file) {
const fileName = (this.fileName = file.name);
const size = file.size;
const shardSize = 1024 * 1024 * 2; //2M
const shardCount = Math.ceil(size / shardSize);
// 切割成多个片段
let shardList = [];
for (let i = 0; i < shardCount; i++) {
let formData = new FormData();
formData.append("fileName", fileName);
formData.append("clipNum", i); //第几块
const start = i * shardSize;
const end = Math.min(size, start + shardSize);
formData.append("content", file.slice(start, end)); //用slice方法切片
shardList.push(formData);
}
this.shardList = shardList;
// 最大上传并发 10/5
let uploadMaxCount =
shardList.length > 10
? 10
: shardList.length > 5
? 5
: shardList.length;
// 设置多个 并行(保证上传更快),递归(保证连续上传)请求
for (let j = 0; j < uploadMaxCount; j++) {
this.uploadApi();
}
},
async uploadApi() {
// 每次请求取出一个片段
const formData = this.shardList.shift();
let ret = await axios.post("/uploadClip", formData);
// 上传失败 push到末尾
if (!ret.data.success) {
this.shardList.push(formData);
}
// 判断是否上传完毕
if (this.shardList.length == 0) {
// 上传全部片段后,发一次合并请求
if (!this.mergeApi) {
this.mergeApi = true;
let ret = await axios.post("/mergeClip", { fileName: this.fileName });
this.mergeApi = false;
console.log(ret.data);
}
} else {
// 继续上传
this.uploadApi();
}
},
},
};
</script>
<style>
.avatar-uploader .el-upload {
border: 1px dashed #d9d9d9;
border-radius: 6px;
cursor: pointer;
position: relative;
overflow: hidden;
}
.avatar-uploader .el-upload:hover {
border-color: #409eff;
}
.avatar-uploader-icon {
font-size: 28px;
color: #8c939d;
width: 178px;
height: 178px;
line-height: 178px;
text-align: center;
}
.avatar {
width: 178px;
height: 178px;
display: block;
}
</style>
后端–临时目录,合并文件
这里使用Gin框架配合 前端,写了两个接口(略)
1.uploadClip接口会生成一个临时目录
2.merClip接口,在前端上传完毕后被调用,用于合并所有文件块
总结
写完这个例子,对文件上传掌握更深了。在封装请求过程中,还尝试过使用Promise.all(),axios.all()等方法,虽然没有直接效果,还是收到了启发。当然现实各种成熟存储技术,编程中一般不需要自己造轮子。