大文件上传流程简述
一、获取文件上传对象
-
获取到文件上传对象可以提取文件名称、大小(bit,通过上传文件的大小可以对上传文件大小进行限制)、类型(通过获取到的文件类型,可以单独对上传文件进行格式化控制)
-
将上传的文件对象切片处理
二、通过spark-md5 获取文件的MD5值
-
该值是文件内容的唯一标识,是唯一的,内容一旦改变,值就会发生改变,文件的名称改变,内容不变,MD5值不变,通过该值给切片命名,目的为了实现断点续传更方便准确。
-
对于大文件,如果直接readAsText读取,速度则会非常慢,可以利用readAsArrayBuffer(file)读取其二进制来计算md5。
// 文件大小较小时 readAsText
var sparkMD5 = new SparkMD5()
var reader = new FileReader()
reader.readAsText(file)
reader.onload = (event) => {
//获取文件MD5
str.value = event.target.result
sparkMD5.append(str.value)
md5.value = sparkMD5.end()
}
// 文件大小较大时 readAsArrayBuffer
var sparkMD5 = new SparkMD5.ArrayBuffer()
var reader = new FileReader()
reader.readAsArrayBuffer(file)
reader.onload = (event) => {
//获取文件MD5
str.value = event.target.result
sparkMD5.append(str.value)
md5.value = sparkMD5.end()
}
三、 切片处理
-
切片方式一:按固定数量切片;
-
切片方式二:按固定一片多少大小切片;
-
切片方式三:二者结合进行切片,按固定一片多少大小进行切片处理,所切出的分片数量与固定数量比较。实际按切片大小切的切片数量大于固定切片数量,按固定数量进行切片;小于固定切片数量,按固定切片大小进行切片(建议使用)。
-
切片命名:由于要传给后端,后端根据上传切片按顺序进行合并操作,合并需要切片顺序,切片顺序通过spark-md5 获取文件的MD5值,命名通过MD5加当前切片序号组成
-
切片的原理:文件对象的slice方法,该方法实际是继承的Blob二进制大对象,返回源Blob对象(即上传的文件二进制对象)中指定范围内的数据file.slice(开始,结束)
四、 切片上传
-
实现文件上传进度管控(即: 真实反应上次切片文件资源进程),在每次上传完切片之后,进度条移动相应距离,实现进度管控,调用上传切片接口,接口请求成功,进度条移动(进度条移动距离 / 进度条总长度 = 上传切片成功个数 / 根据文件切得总个数)
-
上传切片请求是并发的,一次性提交,需获取到所有切片上传完之后的状态,全部上传成功,或者某几片因为网络原因导致上传失败,上传完成之后的状态通过Promise.all来捕获,所有请求只要有一个请求失败最后走catch回调,返回第一个切片请求失败的原因,全部请求成功才会走then回调(最后测试过程中发现当传大文件,并发请求更多,或当前切片过大,改成续传,前一个切片请求成功之后在上传第二个切片)
-
上传切片时难免会有网络波动,所以在Promise.all(...).catch(...)回调中,改变用户在界面的操作,告诉用户传输数据由于网络波动造成上传失败需重新上传,此时重新上传就和断点续传一个想法
注: 在文件切片前可以做一层逻辑判断,先计算当前需要操作的文件大小,指定最小切片文件大小,避免没必要的逻辑处理
五、 断点续传
-
断点续传使用场景1:上传过程中由于网络波动导致的上传失败,需重新上传时(在用户点击继续上传操作时可继续上传上一次失败的切片)
-
断点续传使用场景2: 用户在上传途中出现断网情况,当前操作关闭,不能继续上传操作,在第二次上传同一个上一次未上传完的文件时(在用户上传文件时先区获取已上传的切片,已上传的不需要继续上传,上传上一次未上传的切片)
-
实现原理:通过文件唯一标识的MD5值,后端记录已上传的切片,在未合并切片前,已上传的切片不会被删除,在提交上传切片前先获取后台该文件已存在服务器上的切片,过滤掉已上传的切片,将未上传的切片上传服务器
六、 切片合成
- 在切片全部上传成功到服务器之后,将切片总数(目的: 让后端知道切片是否都上传了),该文件的MD5值传给后端(目的: 告诉后端合并哪一个文件),通知后端合并切片,实现切片合成,获取到已上传视频的服务器URL地址
七、 进度监控
- 进度监控实现想法,拿到每一次切片上传成功状态,进度条向前移动等比比例
建议: 3个后台接口即可,但需要后端紧密配合实现
- 切出的切片上传的接口;
- 上传过的切片服务器已存在切片的接口;
- 合并切片的接口。
- 备注: 各接口间如何配合的-当获取到文件对象之后,将文件对象切片处理,切片之后,在上传切片之前先调接口--已上传切片的接口,循环当前所切得文件切片,通过获取到得已上传切片数组去过滤本次上传,将处理之后需要上传的切片上传,当该文件的所有切片全部上传之后,调用合并接口调整后台合并。
八、 遇到的问题
-
可能存在上传过程中网络问题导致上传失败,解决方案: 由于通过Promise.all并发的请求,一旦有一个请求失败走catch回调,在catch捕获到错误时多做一步网络波动导致上传失败,更改重新上传状态,可通过实现的断点续传功能(即先获取已上传的切片),继续上传
-
当文件过大时,切片数量过高,并发量过大,后端无法承载过大并发量: 不能直接使用Promise.all
解决方法: 优化并发时限制并发数量,根据promise.all实现原理,封装限制并发请求个数。
/**
*
* @param {返回Promise对象的函数数组} fn_return_promise
* @param {并发数量} num
* @returns 返回一个promise对象
*/
function controlNumConRequests(fn_return_promise = [], num = 5) {
let isLegal = true; // 处理参数输入是否合理
JudgmentParameters(fn_return_promise, () => {
isLegal = false;
});
if (!isLegal) {
return;
}
let resultList = [];
let errList = [];
let resloveFn, rejectFn;
let promise = new Promise((resolve, reject) => {
resloveFn = resolve;
rejectFn = reject;
});
let isRunReqList = fn_return_promise.slice(0, num);
let increaseIndex = num;
let endIndex = 0;
function startItemFn(itemFn, index) {
itemFn()
.then((res) => {
endIndex++;
resultList[index] = {
isSuccess: true,
data: res,
};
judgmentResult();
})
.catch((err) => {
endIndex++;
errList.push(err);
resultList[index] = {
isSuccess: false,
data: err,
};
judgmentResult();
});
}
// 每一个异步返回结果都触发一次
function judgmentResult() {
console.log(
`---------------------------开启新任务${increaseIndex} 执行到第: ${endIndex}项; `
);
if (!errList.length && endIndex === fn_return_promise.length) {
resloveFn(resultList);
} else if (errList.length && endIndex === fn_return_promise.length) {
rejectFn(resultList);
} else if (increaseIndex < fn_return_promise.length) {
startItemFn(fn_return_promise[increaseIndex], increaseIndex);
increaseIndex++;
}
}
// 判断传入参数是否合理
function JudgmentParameters(data, callBackErr) {
if (Array.isArray(data)) {
data.forEach((item, index) => {
if (!(item instanceof Function)) {
callBackErr();
throw new Error("传入参数格式不正确,应该数组项由函数组成");
}
if (!(item() instanceof Promise)) {
callBackErr();
throw new Error(
"传入参数格式不正确,应该数组项由函数组成,且返回的是一个promise实例的函数"
);
}
});
} else {
callBackErr();
throw new Error("传入参数类型不对,应该传入数组类型");
}
}
isRunReqList.forEach(startItemFn);
return promise;
}
- 注: 这里只实现的控制并发数量实现限制请求挂起个数一直维持在指定数量,实际结合大文件上传实现进度管控还需要多处理一层逻辑,在每次切片上传成功之后还需要去通知进度条移动