背景
一般上传超过 100MB 算大文件,看各公司定义情况。
大文件上传很慢,影响用户体验。
问题
- 上传很慢
- 网络差的时候,会更慢
- 网络波动会影响上传的速度,还会影响上传是否成功。
- 上传一半,如果失败,影响用户体验
整体流程
分片脑图
分片具体代码
- 创建分片
/*
* params: file 文件对象
* return: 返回分片数组
*/
const createChunks = (file) => {
const result = [];
const size = 1024 * 1024; // 根据公司要求或文件大小,每一片多大
for(let i = 0; i <= file.size; i += size) {
// 文件对象原型自带的 slice 方法进行切割
const chunk = file.slice(i, i + size);
result.push(chunk)
}
return result
}
- 文件命名hash
const hash = (chunks) => {
// 使用MD5进行唯一值处理
const spark = new SparkMD5();
// 通过递归给每个分片进行处理
function _read(index) {
// 先校验,如果到最后一个,结束递归
if(index >= chunks.length) {
spark.end();
return;
}
// 获取其中的一个片段
const blob = chunks[index];
// 创建一个FileReader对象
const reader = new FileReader(blob);
reader.onload = (e) => {
// 拿到当前的文件
const bytes = e.target.result;
// 添加到 spark 中
spark.append(bytes);
// 递归到下一个片段
_read(index + 1)
}
// FileReader 接口的 readAsArrayBuffer() 方法用于开始读取指定 Blob 或 File 的内容。当读取操作完成时,readyState 属性变为 DONE,并触发 loadend 事件。此时,result 属性包含一个表示文件数据的 ArrayBuffer。
reader.readAsArrayBuffer(blob);
}
// 从第一个开始
_read(0)
}
通过上面的代码,可以实现一个同步的上传,但是如果中途某些方法会变为异步的话,会导致阻塞或者报错,所以我们要给这个改为异步处理的,这样既不会影响同步,也不会影响异步。
const hash = (chunks) => {
return new Promise((resolve) => {
// 使用MD5进行唯一值处理
const spark = new SparkMD5();
// 通过递归给每个分片进行处理
function _read(index) {
// 先校验,如果到最后一个,结束递归
if(index >= chunks.length) {
// 假设spark.end 为异步,需要通过resolve包裹
resolve(spark.end());
return;
}
// 获取其中的一个片段
const blob = chunks[index];
// 创建一个FileReader对象
const reader = new FileReader(blob);
reader.onload = (e) => {
// 拿到当前的文件
const bytes = e.target.result;
// 添加到 spark 中
spark.append(bytes);
// 递归到下一个片段
_read(index + 1)
}
// FileReader 接口的 readAsArrayBuffer() 方法用于开始读取指定 Blob 或 File 的内容。当读取操作完成时,readyState 属性变为 DONE,并触发 loadend 事件。此时,result 属性包含一个表示文件数据的 ArrayBuffer。
reader.readAsArrayBuffer(blob);
}
// 从第一个开始
_read(0)
})
}
- 发送分片
/*
* params: 分片数组,分片后的hash数组,文件名称
*
*/
const upload = (chunks, hash, fileName) => {
// 通过promise.all进行上传,如果全部成功会有返回
// 创建一个数组,存储每个上传任务
const taskArr = [];
chunks.forEach((chunk, index) => {
// 暂时我这里使用formData对象进行上传
// 后续根据后端要求进行更改
const formData = new FormData();
formData.append('chunk', chunk);
formData.append('hash', `${hash}-${index}-${fileName}`);
formData.append('fileName', fileName);
const task = axios.post('your-url', formData, {
headers: {
'Content-Type': 'multipart/form-data'
}
})
taskArr.push(task);
})
Promise.all(taskArr).then(res => {
console.log('上传成功', res)
}).catch(err => {
console.log('上传失败', err)
})
}
断点续传脑图
断点续传代码(暂时不支持关闭浏览器后的断点续传)
- 保存已上传的分片
// 使用ref保存已上传的分片
const fileInfoRef = useRef(null); // 保存文件信息:hash, chunks, fileName
// 判断是否是暂停后点击继续操作
// 如果是,并且ref内有值
if(isResume && fileInfoRef.current) {
chunks = fileInfoRef.current.chunks;
hash = fileInfoRef.current.hash;
console.log('继续上传,使用已保存的 hash:', hash);
} else {
chunks = createChunks(file);
try {
// 计算hash(唯一值,为后续断点续传、合并文件做准备)
hash = await createHash(chunks, isCancelledRef);
if (isCancelledRef.current) {
console.log('Hash 计算被取消');
return;
}
// 保存文件信息
fileInfoRef.current = { chunks, hash, fileName: file.name };
} catch (error) {
if (error.message === 'Hash calculation cancelled') {
message.error('Hash 计算被用户取消');
return;
}
message.error('文件哈希计算失败');
console.error(error);
}
}
- 检查上传状态(根据接口查询)
// 先检查已上传的分片
const verifyRes = await axios.post('http://127.0.0.1:3000/verify', {
hash,
fileName,
chunkCount: chunks.length
});
const { uploadedChunks = [], isComplete } = verifyRes.data;
// 如果已完成,直接合并
if (isComplete) {
setPercent(100);
mergeChunks(hash, fileName, chunks);
return;
}
- 检查文件是否存在(接口处理,直接返回是否存在)
做大文件上传想到和遇到的一些问题
当文件上传前计算hash的时候,用户暂停,再继续上传,这个hash计算需要从新计算吗?
答案是: 需要从新计算
原因是:
- hash 是基于文件内容动态计算出来的
- 计算过程是异步的、可中断的
- 你没有缓存中间计算结果(SparkMD5 的中间状态)
好一点的解决方案:
- 增加 UI 展示逻辑,让用户在计算 hash 的时候不能进行暂停