- 分片上传原理:客户端将选择的文件进行切分,每一个分片都单独发送请求到服务端;
- 断点续传 & 秒传原理:客户端
发送请求询问服务端某文件的上传状态
,服务端响应该文件已上传分片,客户端再将未上传分片上传即可;
- 如果没有需要上传的分片就是秒传;
- 如果有需要上传的分片就是断点续传;
- 每个文件要有自己唯一的标识,这个标识就是将整个文件进行MD5加密,这是一个Hash算法,将加密后的Hash值作为文件的唯一标识;
- 使用
第三方工具库,spark-md5是指一个用于计算MD5哈希值的前端JavaScript库
spark-md5
- 文件的合并时机:当服务端确认所有分片都发送完成后,此时会发送请求通知服务端对文件进行合并操作;
如下图所示是前端分片上传的整体流程:
- 第一步:将文件进行分片,并计算其Hash值(文件的唯一标识)
- 第二步:发送请求,询问服务端文件的上传状态
- 第三步:根据文件上传状态进行后续上传
- 文件已经上传过了
- 结束 --- 秒传功能
- 文件已经上传过了
-
- 文件存在,但分片不完整
- 将未上传的分片进行上传 --- 断点续传功能
- 文件存在,但分片不完整
-
- 文件不存在
- 将所有分片上传
- 文件不存在
- 第四步:文件分片全部上传后,发送请求通知服务端合并文件分片
案例实现
前端使用 Element Plus UI
实现文件选择 → 计算 Hash → 分片上传 → 进度显示
假设后端提供接口:
POST /upload/check
→ 接收fileHash
,返回已上传分片列表POST /upload/chunk
→ 上传单个分片POST /upload/merge
→ 所有分片上传完成后通知合并
<template>
<el-upload
:file-list="fileList"
:before-upload="beforeUpload"
:show-file-list="false"
>
<el-button type="primary">选择文件上传</el-button>
</el-upload>
<el-progress
v-if="uploading"
:percentage="uploadProgress"
:text-inside="true"
></el-progress>
</template>
<script setup>
import { ref } from 'vue';
import SparkMD5 from 'spark-md5';
import axios from 'axios';
const fileList = ref([]);
const uploadProgress = ref(0);
const uploading = ref(false);
const chunkSize = 2 * 1024 * 1024; // 2MB
// 计算文件Hash
function calculateFileHash(file) {
return new Promise((resolve, reject) => {
const spark = new SparkMD5.ArrayBuffer();
const fileReader = new FileReader();
const chunks = Math.ceil(file.size / chunkSize);
let currentChunk = 0;
fileReader.onload = e => {
spark.append(e.target.result);
currentChunk++;
if (currentChunk < chunks) {
loadNext();
} else {
resolve(spark.end());
}
};
fileReader.onerror = () => reject('文件读取错误');
function loadNext() {
const start = currentChunk * chunkSize;
const end = Math.min(file.size, start + chunkSize);
fileReader.readAsArrayBuffer(file.slice(start, end));
}
loadNext();
});
}
// 分片上传
async function uploadFileChunks(file, fileHash) {
const chunks = Math.ceil(file.size / chunkSize);
// 先询问服务端已上传分片
const { data } = await axios.post('/upload/check', { fileHash });
const uploadedChunks = data.uploaded || [];
let uploadedCount = 0;
for (let i = 0; i < chunks; i++) {
if (uploadedChunks.includes(i)) {
uploadedCount++;
uploadProgress.value = Math.floor((uploadedCount / chunks) * 100);
continue; // 已上传,跳过
}
const start = i * chunkSize;
const end = Math.min(file.size, start + chunkSize);
const chunkData = file.slice(start, end);
const formData = new FormData();
formData.append('file', chunkData);
formData.append('fileHash', fileHash);
formData.append('index', i);
await axios.post('/upload/chunk', formData, {
onUploadProgress: e => {
// 分片进度可加权到整体进度
const chunkProgress = e.loaded / e.total;
uploadProgress.value = Math.floor(
((uploadedCount + chunkProgress) / chunks) * 100
);
},
});
uploadedCount++;
uploadProgress.value = Math.floor((uploadedCount / chunks) * 100);
}
// 分片上传完成,通知合并
await axios.post('/upload/merge', { fileHash, totalChunks: chunks });
}
// 选择文件上传
async function beforeUpload(file) {
uploading.value = true;
uploadProgress.value = 0;
fileList.value = [file];
// 计算Hash
const fileHash = await calculateFileHash(file);
// 分片上传
await uploadFileChunks(file, fileHash);
uploading.value = false;
ElMessage.success('文件上传完成!');
return false; // 阻止默认上传
}
</script>
1.文件 Hash 的作用是什么?为什么要计算 Hash?
Hash 用作文件的唯一标识,可以判断文件是否已经上传过(秒传),也可以实现断点续传。
同样,合并分片后可以通过 Hash 校验文件完整性。
2.Hash 是怎么计算的?为什么要用增量计算?
使用 FileReader
将文件分片读取,逐块用 SparkMD5
增量计算 Hash。
对大文件一次性计算 Hash 内存占用大且阻塞界面,增量计算避免一次性加载整个文件。
fileReader.readAsArrayBuffer异步读取分片,触发fileReader.onload回调添加到spark中
3.大文件上传可能出现性能瓶颈,你如何优化?
并发上传多分片,充分利用带宽,提高上传速度。
分片大小调节,避免请求次数过多或分片过大导致单次失败。
Hash 计算优化,例如只读取前 N MB + 文件大小组合做快速 Hash。
4.前端上传大量分片时,浏览器内存会不会撑爆?如何避免?
通过分片逐块读取,每次只在内存中处理当前分片,读取完成后释放内存。
5.单个分片上传失败怎么处理?
前端可设置自动重试次数(如 3 次)。
若多次失败,提示用户网络异常或重试。
6.分片上传完成后如何合并?
按分片索引顺序读取所有分片,顺序写入最终文件,生成完整文件。
合并完成后再次计算文件 Hash 或 MD5,与客户端 Hash 比对,如果一致,说明文件完整。