分片上传-
- 分片上传原理:客户端将选择的文件进行切分,每一个分片都单独发送请求到服务端;
- 断点续传 & 秒传原理:客户端
发送请求询问服务端某文件的上传状态
,服务端响应该文件已上传分片,客户端再将未上传分片上传即可;
- 如果没有需要上传的分片就是秒传;
- 如果有需要上传的分片就是断点续传;
- 每个文件要有自己唯一的标识,这个标识就是将整个文件进行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-progressv-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];// 计算Hashconst 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 比对,如果一致,说明文件完整。