大文件上传前言
为了方便大家阅读和理解,我将以单个大文件上传为例,先简单描述下思路。 antd的上传组件有一个上传前的钩子,里面是可以拿到file信息,上传前将file切片,然后包装成一个一个的请求,放到一个数组,上传的的时候将数组的请求执行就可以了,执行完后发送一个合并请求,我没有用Promise.all去执行,而是2个2个的递归执行。
对大文件先通过slice进行切片
核心是利用 Blob.prototype.slice
方法
createFileChunk
接收两个参数
dataSource:所上传的File大文件,size:每个分片大小
//切片
createFileChunk = (dataSource, size = 5 * 1024 * 1024) => {
const fileChunkList = [];//因为只有一个文件,数组只有1项
let cur = 0;
let index = 0;//每个分片给一个索引,最后后端合并按序合并分片
let obj: IFileChunksList = {
name:dataSource.name,
progressArr: [], //记录每一个分片的上传进度
errChunkFile: [],//上传失败的文件
keys: [],//将每个分片包装成一个http请求
};
let arr = [];
while (cur < dataSource.size) {
arr.push(this.createHttp({ hash:dataSource.name '_' index, file: dataSource.slice(cur, cur size)}));
index = 1;
cur = size;
}
obj.keys = arr;
fileChunkList.push(obj);
this.setState({fileChunkList})
};
复制代码
hash
由文件名和序号组成,后端合并的时候需要按顺序合并。
this.createHttp
方法分析
简单的做了参数处理,this.request里面才是真是ajax请求
onProgress:监听ajax进度并实时记录下来
createHttp = (data) => {
const { hash, file } = data;
const formData = new FormData();
formData.append('chunk', file);
formData.append('hash', hash);
return () =>
this.request({
url: this.props.action,
data: formData,
onProgress: this.createProgressHandler(data, hash),
});
};
复制代码
为每个分片创建一个http请求
this.request
方法通过promise和ajax包装
url:分片上传接口。data:分片参数。onProgress:监听此分片上传进度。
requestList:所有正在上传的分片请求集合。(断点续传用的)
也可以在此方法里面设置token认证
//创建ajax
request = ({
url,
method = 'post',
data,
onProgress = (e: any) => e,
requestList = this.state.requestList,
}: IAjax) => {
return new Promise((resolve, reject) => {
const xhr = new XMLHttpRequest();
xhr.upload.onprogress = onProgress;
xhr.open(method, url, true);
xhr.setRequestHeader('Authorization', this.props.token);
xhr.send(data);
xhr.onload = (e: any) => {
// 将请求成功的 xhr 从列表中删除
const { requestList } = this.state;
if (requestList) {
const xhrIndex = requestList.findIndex((item) => item.xhr === xhr);
requestList.splice(xhrIndex, 1);
this.setState({ requestList });
}
resolve({
data: e.target.response,
});
};
xhr.onerror = (e) => {
reject(e);
// throw new Error('fail');
};
requestList.push({ xhr, hash:data.get('hash')});
this.setState({
requestList,
});
});
};
复制代码
记录分片进度方法
this.createProgressHandler(data, hash)
在上面createHttp方法调用
createProgressHandler = (item1, hash) => {
return (e: any) => {
let { fileChunkList } = this.state;
let index=hash.split("_")[1]
fileChunkList[0].progressArr[index]=e.load
this.setState({
fileChunkList,
});
};
};
复制代码
调用开始上传的方法
this.upFile(this,state.fileChunkList[0])(true) 参数true是保证2个请求一发
代码语言:javascript复制 // 开始上传
upFile = ( item) => {
let fileArr=item.keys
let init = 0;
let loopFun = (initValue) => {
fileArr[initValue]()
.then((res) => {
if (JSON.parse(res.data).statusCode === 200) {
init ;
if (init < fileArr.length) {
//继续传下一个分片
loop();
} else if (init === fileArr.length && !item.errChunk.length && fileArr.length !== 1) {
//分片传完,合并分片
this.mergeChunk(item);
}
}
})
.catch((err) => {
//捕获上传失败的分片存起来
let arrChunk = item.errChunkFile.concat(fileArr[initValue]);
init ;
item.errChunkFile = arrChunk;
this.setState({
fileChunkList: [...item],
});
if (init < fileArr.length) {
loop();
}
});
};
let loop = (initFlag) => {
loopFun(init);
if (initFlag) {
loopFun( init);
}
};
return loop;
};
复制代码
合并分片的方法我就不写了,就调用一个接口即可。 假如存在上传失败的分片,会被记录在fileChunkList[0].errChunkFile.对这个失败的数组做一个上传就可以了。
断点续传 暂停
this.state.requestList是当前正在请求的分片集合。暂停就是把请求abort,
代码语言:javascript复制upFileCancel = (itemCurrent: IFileChunksList) => {
this.state.requestList.forEach((item) => {
item.xhr.abort();
});
};
复制代码
续传,可以获取已经上传成功的,然后把未上传的重新上传即可。
总结
我只写了前端的大致实现思想,后端只需提供单个分片上传的接口,合并分片的接口。我的hash用文件名 索引,用spark-md5
对文件内容生成一个hash才是最合适的。
单个大文件上传感觉其实并不复杂,知道它的大致思想再去扩展多文件排队上传,断点续传,记录每个文件的进度条、总进度条甚至每个分片的进度条,还要考虑暂停的时候,由于onProgress是实时监听进度条的,当分片上传了百分之80,取消后变为0,进度条回退的情况....