大文件分片上传完整案例

📅 2026/7/5 2:24:09 👁️ 阅读次数 📝 编程学习
大文件分片上传完整案例

大文件分片上传:完整前后端代码


前端代码

<!DOCTYPEhtml><htmllang="zh-CN"><head><metacharset="UTF-8"><title>大文件上传</title></head><body><inputtype="file"id="fileInput"/><buttononclick="upload()">上传</button><divid="progress"></div><script>constCHUNK_SIZE=5*1024*1024;// 5M 一片asyncfunctionupload(){constfile=document.getElementById('fileInput').files[0];if(!file)return;consttotalChunks=Math.ceil(file.size/CHUNK_SIZE);constfileId=Date.now()+'-'+Math.random().toString(36).substring(2);// 计算文件的 MD5(秒传/校验用,大文件建议用 SparkMD5 增量计算)// 这里省略 MD5 计算,实际可以加上// 逐片上传说到底就是 for 循环for(leti=0;i<totalChunks;i++){conststart=i*CHUNK_SIZE;constend=Math.min(start+CHUNK_SIZE,file.size);constchunk=file.slice(start,end);constformData=newFormData();formData.append('chunk',chunk);formData.append('fileId',fileId);formData.append('chunkIndex',i);formData.append('totalChunks',totalChunks);formData.append('fileName',file.name);// 上传这一片constresp=awaitfetch('/upload/chunk',{method:'POST',body:formData});constresult=awaitresp.json();if(!result.success){document.getElementById('progress').textContent='上传失败,分片 '+i;return;}// 更新进度constpct=Math.round(((i+1)/totalChunks)*100);document.getElementById('progress').textContent=pct+'%';}// 所有分片上传完成,通知后端合并constresp=awaitfetch('/upload/merge',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({fileId,fileName:file.name,totalChunks})});constresult=awaitresp.json();alert('上传完成:'+result.filePath);}</script></body></html>

带并发控制的分片上传

如果一个个上传太慢,可以并发上传,但要注意控制并发数,不然浏览器会把连接占满。

<script>asyncfunctionuploadWithConcurrency(file,maxConcurrent=3){consttotalChunks=Math.ceil(file.size/CHUNK_SIZE);constfileId=Date.now()+'-'+Math.random().toString(36).substring(2);// 把所有分片信息准备好consttasks=[];for(leti=0;i<totalChunks;i++){tasks.push(i);}letcompleted=0;// 控制并发:一次只跑 maxConcurrent 个asyncfunctionworker(){while(tasks.length>0){consti=tasks.shift();conststart=i*CHUNK_SIZE;constend=Math.min(start+CHUNK_SIZE,file.size);constchunk=file.slice(start,end);constformData=newFormData();formData.append('chunk',chunk);formData.append('fileId',fileId);formData.append('chunkIndex',i);formData.append('totalChunks',totalChunks);formData.append('fileName',file.name);constresp=awaitfetch('/upload/chunk',{method:'POST',body:formData});constresult=awaitresp.json();if(!result.success)thrownewError('分片 '+i+' 上传失败');completed++;constpct=Math.round((completed/totalChunks)*100);document.getElementById('progress').textContent=pct+'%';}}// 启动 maxConcurrent 个 workerconstworkers=[];for(leti=0;i<maxConcurrent;i++){workers.push(worker());}awaitPromise.all(workers);// 合并constresp=awaitfetch('/upload/merge',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({fileId,fileName:file.name,totalChunks})});returnawaitresp.json();}</script>

后端代码

Controller 层

@RestController@RequestMapping("/upload")publicclassUploadController{@AutowiredprivateChunkUploadServicechunkUploadService;/** * 接收一个分片 */@PostMapping("/chunk")publicResultuploadChunk(@RequestParam("chunk")MultipartFilechunk,@RequestParam("fileId")StringfileId,@RequestParam("chunkIndex")intchunkIndex,@RequestParam("totalChunks")inttotalChunks,@RequestParam("fileName")StringfileName)throwsIOException{chunkUploadService.saveChunk(fileId,chunkIndex,chunk);returnResult.success();}/** * 合并所有分片 */@PostMapping("/merge")publicResultmergeChunks(@RequestBodyMergeRequestrequest)throwsIOException{StringfilePath=chunkUploadService.merge(request.getFileId(),request.getFileName(),request.getTotalChunks());returnResult.success(filePath);}}

分片上传服务

@ServicepublicclassChunkUploadService{/** 临时分片存放目录 */privatestaticfinalStringCHUNK_DIR="/data/uploads/chunks/";/** 合并后的文件存放目录 */privatestaticfinalStringDEST_DIR="/data/uploads/files/";/** * 保存一个分片到临时目录 */publicvoidsaveChunk(StringfileId,intchunkIndex,MultipartFilechunk)throwsIOException{// 每个文件一个文件夹,存放它的所有分片PathchunkDir=Paths.get(CHUNK_DIR,fileId);Files.createDirectories(chunkDir);// 分片文件命名:0、1、2、3...PathchunkFile=chunkDir.resolve(String.valueOf(chunkIndex));try(FileChannelout=FileChannel.open(chunkFile,StandardOpenOption.WRITE,StandardOpenOption.CREATE,StandardOpenOption.TRUNCATE_EXISTING);FileChannelin=(FileChannel)chunk.getInputStream().getChannel()){longtransferred=0;longfileSize=in.size();while(transferred<fileSize){transferred+=in.transferTo(transferred,fileSize-transferred,out);}}}/** * 合并所有分片 */publicStringmerge(StringfileId,StringfileName,inttotalChunks)throwsIOException{PathchunkDir=Paths.get(CHUNK_DIR,fileId);PathdestFile=Paths.get(DEST_DIR,System.currentTimeMillis()+"_"+fileName);Files.createDirectories(destFile.getParent());try(FileChannelout=FileChannel.open(destFile,StandardOpenOption.WRITE,StandardOpenOption.CREATE,StandardOpenOption.TRUNCATE_EXISTING)){// 按照分片顺序,一个一个写进去for(inti=0;i<totalChunks;i++){PathchunkFile=chunkDir.resolve(String.valueOf(i));if(!Files.exists(chunkFile)){thrownewIOException("分片丢失:"+i);}try(FileChannelin=FileChannel.open(chunkFile,StandardOpenOption.READ)){longtransferred=0;longfileSize=in.size();while(transferred<fileSize){transferred+=in.transferTo(transferred,fileSize-transferred,out);}}}}// 合并完删除临时分片目录deleteChunkDir(chunkDir);returndestFile.toString();}privatevoiddeleteChunkDir(PathchunkDir)throwsIOException{try(Stream<Path>files=Files.list(chunkDir)){files.forEach(path->{try{Files.deleteIfExists(path);}catch(IOExceptionignored){}});}Files.deleteIfExists(chunkDir);}}

DTO

@DatapublicclassMergeRequest{privateStringfileId;privateStringfileName;privateinttotalChunks;}@DatapublicclassResult{privatebooleansuccess=true;privateStringfilePath;publicstaticResultsuccess(){returnnewResult();}publicstaticResultsuccess(StringfilePath){Resultr=newResult();r.filePath=filePath;returnr;}}

用到的关键点

前端

  • file.slice(start, end)— 切分文件,不占额外内存
  • FormData— 传二进制分片,不需要 Base64 编码
  • 并发控制 — 用 worker 模式限制并发数,不要一次性全发出去

后端

  • FileChannel.transferTo— 零拷贝写分片文件和合并
  • 临时分片以fileId/分片序号组织,天然有序
  • 合并完清理临时目录

容错

实际生产还需要补充:

  • 断点续传— 上传前先请求/upload/check?fileId=xxx,后端返回已收到的分片列表,前端跳过这些分片
  • MD5 校验— 前端计算文件 MD5,合并后后端校验是否一致
  • 超时清理— 定时任务清理超过一定时间未合并的临时分片目录
  • 分片大小自适应— 根据网络情况动态调整分片大小(但这个一般不需要,固定 5M 就挺好)