- ?分片上傳原理:客戶端將選擇的文件進行切分,每一個分片都單獨發送請求到服務端;
- 斷點續傳 & 秒傳原理:客戶端
發送請求詢問服務端某文件的上傳狀態
,服務端響應該文件已上傳分片,客戶端再將未上傳分片上傳即可;
- 如果沒有需要上傳的分片就是秒傳;
- 如果有需要上傳的分片就是斷點續傳;
- 每個文件要有自己唯一的標識,這個標識就是將整個文件進行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 比對,如果一致,說明文件完整。