文件上传难点分析
大约 5 分钟
文件上传
断点续传
大文件分包上传
遇到问题
前后端上传请求超时限制,一次性传输大小限制。
网络抖动等,失败后需要重新上传。
http1.1版本, TCP连接默认是open的,所有请求都通过同一个连接进行数据传输,如果前面的请求被阻塞了,后面的请求也得不到响应,也叫HTTP/1.1 中的队头阻塞问题,除非建立多个连接,但是多个连接会浪费资源。
无进度条,用户体验极差。
分包上传
前端
获取文件的二进制内容,然后对其内容拆分成指定大小的切片文件,最后将每个切片上传到服务端即可。
流程:获取文件 ➡️ 分片 ➡️ 上传
需要优化的点
- 中断后无需重新上传(断点续传)
- 上传过的文件无需上传(秒传)
- 显示上传进度
后端
根据切片文件的唯一标识在后端将多个相同文件的切片还原成一个文件
示例代码
// 获取identifier,同一个文件会返回相同的值
function createIdentifiert(file) {
return file.name + file.size
}
let file = document.querySelector("[name=file]").files[0];
const LENGTH = 1024 * 1024 * 1;//1MB
let chunks = slice(file, LENGTH);
// 获取对于同一个文件,获取其identifier
let identifier = createIdentifier(file);
let tasks = [];
chunks.forEach((chunk, index) => {
let fd = new FormData();
//传递file对象
fd.append("file",chunk);
// 传递identifier
fd.append("identifier", identifier);
// 传递切片索引值
fd.append("chunkNumber", index + 1);
// 传递切片总数
fd.append(“totalChunks”, chunks.length);
tasks.push(post("/mkblk.php", fd));
});
// 所有切片上传完毕后,调用mkfile接口
Promise.all(tasks).then(res => {
let fd = new FormData();
fd.append("identifier", identifier);
fd.append("totalChunks",chunks.length);
post("/mkfile.php", fd).then(res => {
console.log(res);
})
});
断点续传
以上代码还需要继续优化的点:断点续传、秒传、上传进度和暂停
1、断点续传
为什么需要断点续传?
- 即使将大文件拆分成切片上传,我们仍需等待所有切片上传完毕,在等待过程中,可能发生一系列导致部分切片上传失败的情形,如网络故障、页面关闭等。由于切片未全部上传,因此无法通知服务端合成文件。这种情况下可以通过断点续传来进行处理。断点续传指的是:可以从已经上传部分开始继续上传未完成的部分,而没有必要从头开始上传,节省上传时间。
怎么实现断点续传?
由于整个上传过程是按切片维度进行的,且mkfile接口是在所有切片上传完成后由客户端主动调用的,因此断点续传的实现也十分简单:
- 在切片上传成功后,保存已上传的切片信息
- 当下次传输相同文件时,遍历切片列表,只选择未上传的切片进行上传
- 所有切片上传完毕后,再调用mkfile接口通知服务端进行文件合并
- 因此问题就落在了如何保存已上传切片的信息了,保存一般有两种策略
- 1.可以通过locaStorage等方式保存在前端浏览器中,这种方式不依赖于服务端,实现起来也比较方便,缺点在于如果用户清除了本地文件,会导致上传记录丢失
- 2.服务端本身知道哪些切片已经上传,因此可以由服务端额外提供一个根据文件context查询已上传切片的接口,在上传文件前调用该文件的历史上传记录
示例代码
// 获取已上传切片记录
function getUploadSliceRecord(context){
let record = localStorage.getItem(context)
if(!record){
return []
}else {
return JSON.parse(record)
}
}
// 保存已上传切片
function saveUploadSliceRecord(context, sliceIndex){
let list = getUploadSliceRecord(context)
list.push(sliceIndex)
localStorage.setItem(context, JSON.stringify(list))
}
let context = createContext(file);
// 获取上传记录
let record = getUploadSliceRecord(context);
let tasks = [];
chunks.forEach((chunk, index) => {
// 已上传的切片则不再重新上传
if(record.includes(index)){
return
}
let fd = new FormData();
fd.append("file", chunk);
fd.append("context", context);
fd.append("chunk", index + 1);
let task = post("/mkblk.php", fd).then(res=>{
// 上传成功后保存已上传切片记录
saveUploadSliceRecord(context, index)
record.push(index)
})
tasks.push(task);
});
...
秒传
什么是秒传?
- 已经上传过的文件,并且在后端已经拼接完成,如果再次上传的话后端不做处理,直接返回拼接好的文件的信息即可,这里主要后端实现,由于篇幅关系,这里不做过多描述。
3、上传进度和暂停
- 通过xhr.upload中的progress方法可以实现监控每一个切片上传进度。
- 上传暂停的实现也比较简单,通过xhr.abort可以取消当前未完成上传切片的上传,实现上传暂停的效果,恢复上传就跟断点续传类似,先获取已上传的切片列表,然后重新发送未上传的切片。
- 由于篇幅关系,上传进度和暂停的功能这里就先不实现了。
参考
https://cloud.tencent.com/developer/article/2391382