
大文件分片上传完整前后端代码前端代码!DOCTYPEhtmlhtmllangzh-CNheadmetacharsetUTF-8title大文件上传/title/headbodyinputtypefileidfileInput/buttononclickupload()上传/buttondividprogress/divscriptconstCHUNK_SIZE5*1024*1024;// 5M 一片asyncfunctionupload(){constfiledocument.getElementById(fileInput).files[0];if(!file)return;consttotalChunksMath.ceil(file.size/CHUNK_SIZE);constfileIdDate.now()-Math.random().toString(36).substring(2);// 计算文件的 MD5秒传/校验用大文件建议用 SparkMD5 增量计算// 这里省略 MD5 计算实际可以加上// 逐片上传说到底就是 for 循环for(leti0;itotalChunks;i){conststarti*CHUNK_SIZE;constendMath.min(startCHUNK_SIZE,file.size);constchunkfile.slice(start,end);constformDatanewFormData();formData.append(chunk,chunk);formData.append(fileId,fileId);formData.append(chunkIndex,i);formData.append(totalChunks,totalChunks);formData.append(fileName,file.name);// 上传这一片constrespawaitfetch(/upload/chunk,{method:POST,body:formData});constresultawaitresp.json();if(!result.success){document.getElementById(progress).textContent上传失败分片 i;return;}// 更新进度constpctMath.round(((i1)/totalChunks)*100);document.getElementById(progress).textContentpct%;}// 所有分片上传完成通知后端合并constrespawaitfetch(/upload/merge,{method:POST,headers:{Content-Type:application/json},body:JSON.stringify({fileId,fileName:file.name,totalChunks})});constresultawaitresp.json();alert(上传完成result.filePath);}/script/body/html带并发控制的分片上传如果一个个上传太慢可以并发上传但要注意控制并发数不然浏览器会把连接占满。scriptasyncfunctionuploadWithConcurrency(file,maxConcurrent3){consttotalChunksMath.ceil(file.size/CHUNK_SIZE);constfileIdDate.now()-Math.random().toString(36).substring(2);// 把所有分片信息准备好consttasks[];for(leti0;itotalChunks;i){tasks.push(i);}letcompleted0;// 控制并发一次只跑 maxConcurrent 个asyncfunctionworker(){while(tasks.length0){constitasks.shift();conststarti*CHUNK_SIZE;constendMath.min(startCHUNK_SIZE,file.size);constchunkfile.slice(start,end);constformDatanewFormData();formData.append(chunk,chunk);formData.append(fileId,fileId);formData.append(chunkIndex,i);formData.append(totalChunks,totalChunks);formData.append(fileName,file.name);constrespawaitfetch(/upload/chunk,{method:POST,body:formData});constresultawaitresp.json();if(!result.success)thrownewError(分片 i 上传失败);completed;constpctMath.round((completed/totalChunks)*100);document.getElementById(progress).textContentpct%;}}// 启动 maxConcurrent 个 workerconstworkers[];for(leti0;imaxConcurrent;i){workers.push(worker());}awaitPromise.all(workers);// 合并constrespawaitfetch(/upload/merge,{method:POST,headers:{Content-Type:application/json},body:JSON.stringify({fileId,fileName:file.name,totalChunks})});returnawaitresp.json();}/script后端代码Controller 层RestControllerRequestMapping(/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{StringfilePathchunkUploadService.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{// 每个文件一个文件夹存放它的所有分片PathchunkDirPaths.get(CHUNK_DIR,fileId);Files.createDirectories(chunkDir);// 分片文件命名0、1、2、3...PathchunkFilechunkDir.resolve(String.valueOf(chunkIndex));try(FileChanneloutFileChannel.open(chunkFile,StandardOpenOption.WRITE,StandardOpenOption.CREATE,StandardOpenOption.TRUNCATE_EXISTING);FileChannelin(FileChannel)chunk.getInputStream().getChannel()){longtransferred0;longfileSizein.size();while(transferredfileSize){transferredin.transferTo(transferred,fileSize-transferred,out);}}}/** * 合并所有分片 */publicStringmerge(StringfileId,StringfileName,inttotalChunks)throwsIOException{PathchunkDirPaths.get(CHUNK_DIR,fileId);PathdestFilePaths.get(DEST_DIR,System.currentTimeMillis()_fileName);Files.createDirectories(destFile.getParent());try(FileChanneloutFileChannel.open(destFile,StandardOpenOption.WRITE,StandardOpenOption.CREATE,StandardOpenOption.TRUNCATE_EXISTING)){// 按照分片顺序一个一个写进去for(inti0;itotalChunks;i){PathchunkFilechunkDir.resolve(String.valueOf(i));if(!Files.exists(chunkFile)){thrownewIOException(分片丢失i);}try(FileChannelinFileChannel.open(chunkFile,StandardOpenOption.READ)){longtransferred0;longfileSizein.size();while(transferredfileSize){transferredin.transferTo(transferred,fileSize-transferred,out);}}}}// 合并完删除临时分片目录deleteChunkDir(chunkDir);returndestFile.toString();}privatevoiddeleteChunkDir(PathchunkDir)throwsIOException{try(StreamPathfilesFiles.list(chunkDir)){files.forEach(path-{try{Files.deleteIfExists(path);}catch(IOExceptionignored){}});}Files.deleteIfExists(chunkDir);}}DTODatapublicclassMergeRequest{privateStringfileId;privateStringfileName;privateinttotalChunks;}DatapublicclassResult{privatebooleansuccesstrue;privateStringfilePath;publicstaticResultsuccess(){returnnewResult();}publicstaticResultsuccess(StringfilePath){ResultrnewResult();r.filePathfilePath;returnr;}}用到的关键点前端file.slice(start, end)— 切分文件不占额外内存FormData— 传二进制分片不需要 Base64 编码并发控制 — 用 worker 模式限制并发数不要一次性全发出去后端FileChannel.transferTo— 零拷贝写分片文件和合并临时分片以fileId/分片序号组织天然有序合并完清理临时目录容错实际生产还需要补充断点续传— 上传前先请求/upload/check?fileIdxxx后端返回已收到的分片列表前端跳过这些分片MD5 校验— 前端计算文件 MD5合并后后端校验是否一致超时清理— 定时任务清理超过一定时间未合并的临时分片目录分片大小自适应— 根据网络情况动态调整分片大小但这个一般不需要固定 5M 就挺好