突然被问到大文件如何上传,脑海中并么有完整的解决思路, 简单整理下
理论
- 将需要上传的文件转化为blob流的格式
- 利用slice分割二进制流,将流分割成相同大小
- 使用formData包装参数,并行/串行发送请求,
- 待所有情求发送完毕,给后端发送合并信号进行文件合并
实践
使用antd的upload 组件进行模拟.
<Upload beforeUpload={beforeUpload}>
<Button icon={<UploadOutlined />}>Upload</Button>
</Upload>
在获取到文件流之后进行等量分割得到由文件流组成的数组
const sizeInfo = 1024 * 1000;
const beforeUpload = (info) => {
const createFileChunk = (data, size = sizeInfo) => {
const fileChunkList = [];
let cur = 0;
while (cur < data.size) {
fileChunkList.push({ file: data.slice(cur, cur + size) });
cur += size;
}
return fileChunkList;
};
const fileChunkList = createFileChunk(info);
return false;
};
接下来拼接请求数据上传文件
// 模拟的接口
const mockQuery = (data, item) => {
return new Promise((resolve, reject) => {
axios({
method: "post",
url: "xxxxxxxxxxxxxxxxxx",
data: data,
headers: {
"Content-Type": "multipart/form-data",
},
})
.then((res) => {
resolve(item.index);
})
.catch((err) => {
reject(item.index);
});
});
};
const beforeUpload = (info) => {
const fileChunkList = createFileChunk(info);
const data = fileChunkList.map((item, index) => {
return {
file: item?.file,
index: `${info?.name}-${index}`, // 保存下标并追踪记录
};
});
const queryList = data.map((item) => {
const formData = new FormData();
formData.append("file", item?.file);
formData.append("index", item?.index);
formData.append("filename", this.container.file.name);
return mockQuery(formData, item);
});
Promise.all(queryList).then((res) => {
console.log(res);
}).catch((err) => {
console.log(err)
});
return false;
};
诸多问题
文件秒传
文件秒传是指在文件上传时服务端已经存在了上传的资源,所以在文件再次上传时提示上传成功。 实现文件秒传需要对上传的不同文件生成唯一的hash名称,但是对于大文件计算hash比较浪费性能,建议在web-worker中进行计算。后端如果找到相同的hash文件则直接返回成功信息。
并发控制
如果分割的文件流过多,可能会发出很多的情求,所以进行并发数量的控制是很有必要的。
const sendQuery = (list, max = 6) => {
const arr = list;
const newList = list.splice(0, max - 1);
Promise.all(newList)
.then((res) => {
if (arr.length !== 0) {
sendQuery(arr, max);
}
})
.catch((err) => {
console.log(err);
});
};
// queryList 上面请求数组
sendQuery(queryList);
断点续传与报错补传
断点续传
如果因为特殊原因刷新页面或者网络情求出错,我们不需要将已上传的部分再次上传,而是从断开的那个流开始继续上传,这就需要我们在接口返回成功的地方进行处理,记录以及上传的流并保存下来然后从中断的那部分开始上传。
报错补传
假设在上传过程中某一个片段上传失败,我们不需要将所有重新上传,我们只需要将失败的片段重新上传即可,所以我们在接口返回结果中进行处理,记录下失败的片段,并在前面的所有片段请求完成后进行上传。
const sendQuery = (list, max = 6) => {
const arr = list;
const newList = list.splice(0, max - 1);
Promise.all(newList)
.then((res) => {
// 在这里拿到成功的index并储存在sessionStory
// 对于成功的片段在queryList里删除掉,如果上传失败则继续存放在queryList里面继续上传。
// spliceData -删除queryList中上传成功的情求方法。
const surplusData = spliceData(res, queryList);
if (arr.length !== 0) {
sendQuery(surplusData, max);
}
})
.catch((err) => {
console.log(err);
});
};