大文件分片上传、断点续传、秒传
前言
- 也是很久没有更新了,这段时间的文章都写在了飞书云里面,后续也会慢慢同步过来。
- 最近面试的时候被问到有关大文件分片上传的问题,是为后端工程师面的,虽然接触过一点,但并没有系统性的总结,还是有一定的作用的。
- 其实也是为了方便在工作中可以复制<小声bibi>,
- 本文章为他文总结性,
本文主要讲述了大文件的分片上传、断点续传和秒传三个功能。实际上,断点续传和秒传都是基于分片上传扩展的功能。
分片上传原理:客户端将选择的文件进行切分,每一个分片都单独发送请求到服务端。
断点续传 & 秒传原理:客户端发送请求询问服务端某文件的上传状态,服务端响应该文件已上传分片,客户端再将未上传分片上传即可。
- 如果没有需要上传的分片就是秒传;
- 如果有需要上传的分片就是断点续传。
文件唯一标识:每个文件要有自己唯一的标识,这个标识就是将整个文件进行MD5加密,这是一个Hash算法,将加密后的Hash值作为文件的唯一标识。可以使用第三方工具库spark-md5。
文件合并时机:当服务端确认所有分片都发送完成后,此时会发送请求通知服务端对文件进行合并操作。
前端整体流程
分片计算Hash值:首先将文件进行分片,并计算其Hash值(文件的唯一标识)。
查询上传状态:发送请求,询问服务端文件的上传状态。
根据上传状态进行操作:
- 文件已经上传过了:
- 结束,实现秒传功能。
- 文件存在,但分片不完整:
- 将未上传的分片进行上传,实现断点续传功能。
- 文件不存在:
- 将所有分片上传。
- 文件已经上传过了:
合并文件:当文件分片全部上传后,发送请求通知服务端合并文件分片。
详细流程
上传文件
上传文件触发回调函数,同时注意需要的参数:
/**
* @param {File} file 目标上传文件
* @param {number} baseChunkSize 上传分块大小,单位Mb
* @param {string} uploadUrl 上传文件的后端接口地址
* @param {string} vertifyUrl 验证文件上传的接口地址
* @param {string} mergeUrl 请求进行文件合并的接口地址
* @param {Function} progress_cb 更新上传进度的回调函数
* @returns {Promise}
*/
async function uploadFile(file, baseChunkSize, uploadUrl, vertifyUrl, mergeUrl, progress_cb) {
...
}
- 将文件进行分片并计算Hash值
得到 allChunkList—所有分片 fileHash—文件的hash值- 通过fileHash请求服务端,判断文件上传状态
得到 neededFileList—待上传文件分片- 同步上传进度,针对不同文件上传状态调用 progress_cb
- 发送上传请求
- 发送文件合并请求
文件分片 & Hash计算
/**
* 将目标文件分片 并 计算文件Hash
* @param {File} targetFile 目标上传文件
* @param {number} baseChunkSize 上传分块大小,单位Mb
* @returns {chunkList:ArrayBuffer,fileHash:string}
*/
async function sliceFile(targetFile, baseChunkSize = 1) {
return new Promise((resolve, reject) => {
//初始化分片方法,兼容问题
let blobSlice = File.prototype.slice || File.prototype.mozSlice || File.prototype.webkitSlice;
//分片大小 baseChunkSize Mb
let chunkSize = baseChunkSize * 1024 * 1024;
//分片数
let targetChunkCount = targetFile && Math.ceil(targetFile.size / chunkSize);
//当前已执行分片数
let currentChunkCount = 0;
//当前以收集的分片
let chunkList = [];
//创建sparkMD5对象
let spark = new SparkMD5.ArrayBuffer();
//创建文件读取对象
let fileReader = new FileReader();
let fileHash = null;
//FilerReader onload事件
fileReader.onload = (e) => {
//当前读取的分块结果 ArrayBuffer
const curChunk = e.target.result;
//将当前分块追加到spark对象中
spark.append(curChunk);
currentChunkCount++;
chunkList.push(curChunk);
//判断分块是否全部读取成功
if (currentChunkCount >= targetChunkCount) {
//全部读取,获取文件hash
fileHash = spark.end();
resolve({ chunkList, fileHash });
} else {
loadNext();
}
};
//FilerReader onerror事件
fileReader.onerror = () => {
reject(null);
};
//读取下一个分块
const loadNext = () => {
//计算分片的起始位置和终止位置
const start = chunkSize * currentChunkCount;
let end = start + chunkSize;
if (end > targetFile.size) {
end = targetFile.size;
}
//读取文件,触发onLoad
fileReader.readAsArrayBuffer(blobSlice.call(targetFile, start, end));
};
loadNext();
});
}
分片上传入口完善
async function uploadFile(file, baseChunkSize, uploadUrl, vertifyUrl, mergeUrl, progress_cb) {
const { chunkList, fileHash } = await sliceFile(file, baseChunkSize);
//所有分片 ArrayBuffer[]
let allChunkList = chunkList;
//需要上传的分片序列 number[]
let neededChunkList = [];
//上传进度
let progress = 0;
//发送请求,获取文件上传状态
if (vertifyUrl) {
// 这里发送请求
const { data } = await requestInstance.post(vertifyUrl, {
fileHash,
totalCount: allChunkList.length,
extname: '.' + file.name.split('.').pop(),
});
const { neededFileList, message } = data;
if (message) console.info(message);
//无待上传文件,秒传
if (!neededFileList.length) {
progress_cb(100);
return;
}
//部分上传成功,更新unUploadChunkList
neededChunkList = neededFileList;
}
//同步上传进度,断点续传情况下
progress = ((allChunkList.length - neededChunkList.length) / allChunkList.length) * 100;
//上传
if (allChunkList.length) {
const requestList = allChunkList.map(async (chunk, index) => {
if (neededChunkList.includes(index + 1)) {
const response = await uploadChunk(chunk, index + 1, fileHash, uploadUrl);
//更新进度
progress += Math.ceil(100 / allChunkList.length);
if (progress >= 100) progress = 100;
progress_cb(progress);
return response;
}
});
Promise.all(requestList).then(() => {
//发送请求,通知后端进行合并 //后缀名可通过其他方式获取,这里以.mp4为例
requestInstance.post(mergeUrl, { fileHash, extname: '.mp4' });
});
}
}
在上传时我们调用了uploadChunk()
方法,由于我们的请求不仅包含文件,还包含分片索引以及hash值,因此我们的请求体应该是formData,还有一点需要就是此时我们传入的chunk的类型是ArrayBuffer
,而formData中文件的类型应该是Blob
。 具体代码如下:
async function uploadChunk(chunk, index, fileHash, uploadUrl) {
let formData = new FormData();
//注意这里chunk在之前切片之后未ArrayBuffer,而formData接收的数据类型为 blob|string
formData.append('chunk', new Blob([chunk]));
formData.append('index', index);
formData.append('fileHash', fileHash);
return requestInstance.post(uploadUrl, formData);
}