大文件上传:秒传、断点续传、分片上传
文章目录
- 大文件上传:秒传、断点续传、分片上传
- 前言
- 一、大文件上传的挑战
- 二、秒传
- 三、分片上传
- 四、断点续传
- 总结
前言
大文件上传是 Web 开发中高频且复杂的需求,核心痛点集中在传输稳定性、效率、服务器压力三大维度。秒传、断点续传、分片上传是解决这些问题的三大核心技术,三者并非孤立存在,而是常组合使用
一、大文件上传的挑战
- 在深入技术前,需明确大文件上传(通常指≥100MB 的文件,如视频、大型压缩包底层逻辑:
- 传输层面:网络波动(导致上传中断,需重新上传全部内容,浪费时间;
- 服务器层面:单次接收大文件占用大量内存 / 磁盘 IO,易引发服务器过载,甚至触发连接超时;
- 存储层面:重复文件多次上传导致存储冗余,浪费磁盘空间;
- 限制层面:浏览器、服务器对单次上传文件大小有默认限制,直接上传大文件会报错;
- 用户体验层面:上传耗时久,无进度反馈,中断后需重新开始,体验极差
- 三大技术分别针对不同痛点:
- 秒传:解决「重复文件存储 / 传输」问题;
- 分片上传:解决「单次传输压力大、大小限制」问题;
- 断点续传:解决「传输中断后重新上传」问题。
二、秒传
- 核心定义
秒传是指用户上传文件时,服务器已存在相同文件,无需传输任何文件数据,仅通过「文件唯一标识校验」直接返回上传成功的技术,全程耗时通常≤300ms。 - 核心原理
基于「文件内容唯一性校验」,核心逻辑是:
- 对文件内容生成唯一标识(哈希值),相同文件的哈希值绝对一致;
- 上传前先向服务器查询该哈希值是否存在,存在则直接关联文件,不存在则执行正常上传。
三、分片上传
- 核心定义
将大文件按固定大小拆分为多个「小分片」(如 1MB、5MB),通过多请求并行 / 串行上传分片,服务器接收后按顺序合并为完整文件的技术。核心是「分而治之」,降低单次传输的文件体积。 - 核心原理
- 前端:将文件拆分为 N 个分片,为每个分片分配唯一索引(如 0、1、2…N-1),记录总分片数、分片大小;
- 后端:接收分片并临时存储,待所有分片上传完成后,按索引顺序合并为完整文件,最后校验文件完整性。
关键技术细节
(1) 分片拆分规则
- 分片大小选择:
推荐范围:1MB~10MB;
过小:请求次数过多(如 1GB 文件拆分为 1KB 分片,需 100 万次请求),加重服务器压力;
过大:失去分片意义(如 1GB 文件拆分为 500MB 分片,与直接上传无差异); - 动态适配:根据网络速度调整(网络好→分片大,网络差→分片小),可通过navigator.connection.effectiveType获取网络类型。
- 分片索引与标识:
每个分片需携带「文件唯一标识(fileHash)+ 分片索引(chunkIndex)+ 总分片数(totalChunks)+ 分片哈希(chunkHash)」; - 注意:fileHash 用于关联同一文件的所有分片,chunkIndex 用于合并时排序,chunkHash 用于校验分片完整性。
(2)分片上传策略
- 串行上传:按分片索引顺序逐一上传,上一个分片成功后再上传下一个;
优点:逻辑简单,无并发冲突;
缺点:上传速度慢; - 并行上传:同时上传多个分片;
优点:充分利用带宽,提升上传速度;
缺点:需控制并发数(推荐 3~5 个),避免服务器连接数耗尽; - 优先级上传:先上传前几个分片(如前 3 个),用于文件预览(如视频封面生成),再并行上传剩余分片。
(3)服务器分片处理
- 临时存储:分片上传时,服务器需将分片存储在临时目录(如/temp/{fileHash}/{chunkIndex});
- 分片校验:接收分片后,计算分片哈希并与客户端传递的 chunkHash 对比,不一致则拒绝存储,要求重传;
- 合并触发:客户端所有分片上传完成后,发送「合并请求」,服务器触发合并逻辑;
- 合并逻辑:按 chunkIndex 顺序读取所有分片,写入目标文件,合并完成后删除临时分片目录;
- 完整性校验:合并后计算完整文件的哈希值,与客户端传递的 fileHash 对比,一致则上传成功。
四、断点续传
- 核心定义
当上传中断(断网、浏览器关闭、客户端崩溃)后,再次上传同一文件时,无需从头开始,仅上传未完成的分片 / 部分的技术。核心是「记录上传进度」。 - 核心原理
- 上传进度记录:通过「客户端本地存储」或「服务器存储」记录已上传的分片 / 文件位置;
- 断点恢复:再次上传时,查询已上传进度,仅传输未完成部分;
- 依赖关系:断点续传通常依赖分片上传(将文件拆分为分片后,进度可精确到「分片级别」)。
- 关键技术细节
(1)进度记录方式
- 客户端存储:
存储位置:localStorage(小容量,适合存储分片索引列表)、IndexedDB(大容量,适合存储大文件进度);
存储内容:fileHash→{已上传分片索引列表、文件元信息};
优点:无需服务器参与,查询速度快;
缺点:仅支持本设备续传,清除浏览器数据后进度丢失; - 服务器存储:
存储位置:数据库(MySQL/PostgreSQL)或缓存(Redis);
存储内容:fileHash→{已上传分片索引列表、文件元信息、上传时间戳};
优点:支持跨设备续传(如手机上传一半,电脑继续传);
缺点:增加服务器存储压力,需定时清理过期进度记录。
(2)断点类型与处理 - 主动中断(用户手动暂停):
客户端:记录当前已上传分片列表,暂停所有未完成上传请求;
恢复:用户点击继续后,查询已上传分片,仅上传缺失部分; - 被动中断(断网、浏览器关闭):
客户端:下次打开页面时,通过 fileHash 查询本地 / 服务器的进度记录;
恢复:自动触发续传,或提示用户是否继续上传; - 过期中断(上传长时间未完成):
服务器:设置过期时间(如 24 小时),超过时间未完成上传则删除已存储的分片和进度记录;
客户端:再次上传时,服务器返回「文件已过期」,需重新上传。
举例子:
<template><divclass="upload-container"><h2>Vue3 大文件上传(秒传+分片+断点续传)</h2><!--文件选择--><input type="file"ref="fileInput"@change="handleFileSelect"class="file-input"/><!--操作按钮--><divclass="btn-group"><button@click="handleUpload":disabled="!file || isUploading">{{isUploading?'上传中...':'开始上传'}}</button><button@click="handlePause":disabled="!isUploading">暂停上传</button></div><!--进度展示--><divclass="progress-wrap"><divclass="progress-bar":style="{ width: `${progress}%` }"></div></div><pclass="progress-text">进度:{{progress}}%</p><!--状态提示--><pclass="status">{{statusText}}</p></div></template><script setup>import{ref,reactive}from'vue'importaxios from'axios'importSparkMD5 from'spark-md5'// 核心配置constBASE_URL='http://localhost:3000'// 后端地址constCHUNK_SIZE=5*1024*1024// 分片大小:5MBconstCONCURRENT_NUM=3// 并行上传数:3个// 响应式数据constfileInput=ref(null)constfile=ref(null)// 选中的文件constfileHash=ref('')// 文件唯一哈希constprogress=ref(0)// 上传进度constisUploading=ref(false)// 是否正在上传conststatusText=ref('请选择文件开始上传')// 状态提示constuploadedChunks=ref([])// 已上传分片索引constuploadTasks=ref([])// 上传任务队列(用于暂停)constisPaused=ref(false)// 是否暂停/** * 步骤1:选择文件 */consthandleFileSelect=(e)=>{constselectedFile=e.target.files[0]if(!selectedFile)returnfile.value=selectedFile statusText.value=`已选择文件:${selectedFile.name}(${formatSize(selectedFile.size)})` progress.value=0}/** * 辅助函数:格式化文件大小(字节→MB/KB) */constformatSize=(bytes)=>{if(bytes<1024)return`${bytes}B`if(bytes<1024*1024)return`${(bytes/1024).toFixed(2)}KB`return`${(bytes/(1024*1024)).toFixed(2)}MB`}/** * 步骤2:计算文件哈希(秒传/断点续传的核心标识) */constcalculateFileHash=async(file)=>{returnnewPromise((resolve)=>{statusText.value='正在计算文件哈希...'constspark=newSparkMD5.ArrayBuffer()constreader=newFileReader()constslice=File.prototype.slice||File.prototype.mozSlice||File.prototype.webkitSliceconsttotalChunks=Math.ceil(file.size/CHUNK_SIZE)let currentChunk=0// 递归读取分片计算哈希(避免大文件卡顿)constloadNextChunk=()=>{conststart=currentChunk*CHUNK_SIZEconstend=Math.min(start+CHUNK_SIZE,file.size)reader.readAsArrayBuffer(slice.call(file,start,end))}reader.onload=(e)=>{spark.append(e.target.result)currentChunk++// 进度提示statusText.value=`计算哈希中:${Math.ceil((currentChunk/totalChunks)*100)}%`if(currentChunk>=totalChunks){consthash=spark.end()fileHash.value=hashresolve(hash)return}loadNextChunk()}loadNextChunk()})}/** * 步骤3:秒传校验 + 断点续传查询 */constcheckUploadStatus=async(hash,fileName)=>{try{constres=awaitaxios.post(`${BASE_URL}/check`,{fileHash:hash,fileName:fileName})if(res.data.success){// 秒传成功:直接完成progress.value=100statusText.value=`秒传成功!文件地址:${res.data.fileUrl}` isUploading.value=falsereturntrue}else{// 秒传失败:获取已上传分片(断点续传)uploadedChunks.value=res.data.uploadedChunks||[]statusText.value=`秒传失败,已上传分片数:${uploadedChunks.value.length}`returnfalse}}catch(err){statusText.value=`校验失败:${err.message}` isUploading.value=falsereturnfalse}}/** * 步骤4:上传单个分片 */constuploadChunk=async(chunk,chunkIndex,totalChunks)=>{constformData=newFormData()formData.append('fileHash',fileHash.value)formData.append('chunkIndex',chunkIndex)formData.append('totalChunks',totalChunks)formData.append('chunk',chunk)try{awaitaxios.post(`${BASE_URL}/upload-chunk`,formData,{onUploadProgress:(e)=>{// 计算整体进度constchunkProgress=(e.loaded/e.total)/totalChunks*100constbaseProgress=(uploadedChunks.value.length/totalChunks)*100progress.value=Math.min(baseProgress+chunkProgress,100)}})// 记录已上传分片uploadedChunks.value.push(chunkIndex)returntrue}catch(err){statusText.value=`分片${chunkIndex}上传失败:${err.message}`returnfalse}}/** * 步骤5:合并分片 */constmergeChunks=async(totalChunks)=>{try{constres=awaitaxios.post(`${BASE_URL}/merge`,{fileHash:fileHash.value,fileName:file.value.name,totalChunks:totalChunks})if(res.data.success){progress.value=100statusText.value=`上传成功!文件地址:${res.data.fileUrl}`}else{statusText.value='分片合并失败'}isUploading.value=false}catch(err){statusText.value=`合并失败:${err.message}` isUploading.value=false}}/** * 核心:开始上传 */consthandleUpload=async()=>{if(!file.value){statusText.value='请先选择文件'return}if(isUploading.value&&isPaused.value){// 暂停后继续上传isPaused.value=falsestatusText.value='恢复上传...'return}isUploading.value=trueisPaused.value=falsestatusText.value='初始化上传...'// 1. 计算文件哈希awaitcalculateFileHash(file.value)// 2. 秒传校验 + 断点查询constisInstantUpload=awaitcheckUploadStatus(fileHash.value,file.value.name)if(isInstantUpload)return// 3. 分片上传consttotalChunks=Math.ceil(file.value.size/CHUNK_SIZE)constunUploadedChunks=[]// 筛选未上传的分片for(let i=0;i<totalChunks;i++){if(!uploadedChunks.value.includes(i)){unUploadedChunks.push(i)}}if(unUploadedChunks.length===0){// 所有分片已上传,直接合并awaitmergeChunks(totalChunks)return}statusText.value=`开始上传分片(共${totalChunks}片,待传${unUploadedChunks.length}片)`// 并行上传(控制并发数)let index=0constuploadNext=async()=>{if(isPaused.value||index>=unUploadedChunks.length)returnconstchunkIndex=unUploadedChunks[index]index++// 切割分片conststart=chunkIndex*CHUNK_SIZEconstend=Math.min(start+CHUNK_SIZE,file.value.size)constchunk=file.value.slice(start,end)// 上传当前分片consttask=uploadChunk(chunk,chunkIndex,totalChunks).then(()=>{uploadTasks.value=uploadTasks.value.filter(t=>t!==task)// 递归上传下一个uploadNext()})uploadTasks.value.push(task)}// 启动并发上传for(let i=0;i<CONCURRENT_NUM;i++){uploadNext()}// 等待所有分片上传完成awaitPromise.all(uploadTasks.value)if(!isPaused.value){// 合并分片awaitmergeChunks(totalChunks)}}/** * 暂停上传 */consthandlePause=()=>{isPaused.value=trueisUploading.value=falsestatusText.value='已暂停上传,可点击「开始上传」恢复'}</script><style scoped>.upload-container{width:600px;margin:50px auto;padding:20px;border:1px solid #eee;border-radius:8px;}.file-input{margin-bottom:20px;}.btn-group{margin-bottom:15px;}button{padding:8px20px;margin-right:10px;background:#409eff;color:#fff;border:none;border-radius:4px;cursor:pointer;}button:disabled{background:#ccc;cursor:not-allowed;}.progress-wrap{height:10px;width:100%;background:#eee;border-radius:5px;margin-bottom:10px;}.progress-bar{height:100%;background:#409eff;border-radius:5px;transition:width0.3s;}.progress-text{margin:0010px0;color:#666;}.status{color:#333;line-height:1.5;}</style>总结
以上就是今天要讲的内容,本文仅仅简单介绍了大文件上传:秒传、断点续传、分片上传知识点,熟练掌握还是需要去试实践