亚洲乱码中文字幕综合,中国熟女仑乱hd,亚洲精品乱拍国产一区二区三区,一本大道卡一卡二卡三乱码全集资源,又粗又黄又硬又爽的免费视频

基于JavaScript+SpringBoot實(shí)現(xiàn)大文件分片上傳

 更新時(shí)間:2025年06月10日 08:58:02   作者:Micro麥可樂(lè)  
在很多Web應(yīng)用場(chǎng)景下,我們需要上傳體積很大的文件(視頻、鏡像包、數(shù)據(jù)包等),一次性將整個(gè)文件上傳往往會(huì)面臨很多問(wèn)題,為了解決這些問(wèn)題,分片上傳(Chunked Upload)應(yīng)運(yùn)而生,本文將帶著小伙伴了解如何基于前端原生JavaScript+SpringBoot實(shí)現(xiàn)大文件分片上傳

1. 前言

在很多 Web 應(yīng)用場(chǎng)景下,我們需要上傳體積很大的文件(視頻、鏡像包、數(shù)據(jù)包等)。一次性將整個(gè)文件上傳往往會(huì)面臨以下問(wèn)題:

  • 網(wǎng)絡(luò)不穩(wěn)定時(shí)容易中斷:導(dǎo)致上傳失敗,需要重頭再來(lái)
  • 服務(wù)器內(nèi)存/磁盤壓力大:一次性接收大文件可能瞬間占滿帶寬或?qū)憹M臨時(shí)目錄
  • 用戶體驗(yàn)差:上傳過(guò)程中無(wú)法做到斷點(diǎn)續(xù)傳或重試

為了解決上述問(wèn)題,分片上傳(Chunked Upload)應(yīng)運(yùn)而生。它將大文件拆分成一個(gè)個(gè)小塊,按序上傳并在后臺(tái)合并,既可以實(shí)現(xiàn)斷點(diǎn)續(xù)傳,也能平滑流量、降低服務(wù)器壓力。

本文博主將帶著小伙伴了解如何基于 前端原生 JavaScript + Spring Boot 實(shí)現(xiàn)大文件分片上傳。

2. 為什么要分片

  • 斷點(diǎn)續(xù)傳
    每個(gè)分片上傳完成后都會(huì)得到確認(rèn),下次重試只需上傳未成功的分片,用戶體驗(yàn)更佳。
  • 可控并發(fā)
    前端可以設(shè)置并發(fā)上傳的分片數(shù)量(比如同時(shí) 3~5 個(gè)),既能提高吞吐量,又不至于瞬時(shí)壓垮網(wǎng)絡(luò)或服務(wù)器。
  • 流量均衡
    小塊數(shù)據(jù)平滑地傳輸,避免一次性大流量沖擊。
  • 兼容性與安全
    后端可對(duì)每個(gè)分片做校驗(yàn)(大小、哈希、格式等),在合并前即可過(guò)濾非法內(nèi)容。

分片上傳的核心優(yōu)勢(shì)

痛點(diǎn)分片方案收益
超時(shí)中斷小片獨(dú)立上傳避免整體失敗
內(nèi)存壓力單片流式處理內(nèi)存占用<10MB
網(wǎng)絡(luò)波動(dòng)失敗分片重試帶寬利用率提升40%+
大文件傳輸并行上傳機(jī)制速度提升3-5倍
意外中斷斷點(diǎn)續(xù)傳支持節(jié)省90%重復(fù)流量

3. 實(shí)現(xiàn)思路與流程

前端

用戶選中文件后,按固定大?。ㄈ?1MB)切片;
依次(或并發(fā))將每個(gè)分片通過(guò) fetch/XMLHttpRequest 上傳到后端;
上傳完所有分片后,通知后端開(kāi)始合并;

后端(Spring Boot)

接收每個(gè)分片時(shí),根據(jù)文件唯一標(biāo)識(shí)(如 MD5)與分片序號(hào),保存到臨時(shí)目錄;
接收 “合并請(qǐng)求” 時(shí),按序讀取所有分片并寫入最終文件;
合并完成后,可刪除臨時(shí)分片,返回成功。

4. 完整實(shí)現(xiàn)方案

1、前端分片邏輯實(shí)現(xiàn)

首先我們編寫前端的分片、上傳邏輯

<input type="file" id="largeFile">
<button onclick="startUpload()">開(kāi)始上傳</button>
<div id="progressBar"></div>

<script>
async function startUpload() {
  const file = document.getElementById('largeFile').files[0];
  if (!file) return;
  
  // 配置參數(shù)
  const CHUNK_SIZE = 5 * 1024 * 1024; // 5MB分片
  const TOTAL_CHUNKS = Math.ceil(file.size / CHUNK_SIZE);
  const FILE_ID = `${file.name}-${file.size}-${Date.now()}`;
  
  // 創(chuàng)建進(jìn)度跟蹤器
  const uploadedChunks = new Set();
  
  // 并行上傳控制(最大5并發(fā))
  const parallelLimit = 5;
  let currentUploads = 0;
  let activeChunks = 0;
  
  for (let chunkIndex = 0; chunkIndex < TOTAL_CHUNKS; ) {
    if (currentUploads >= parallelLimit) {
      await new Promise(resolve => setTimeout(resolve, 500));
      continue;
    }
    
    if (uploadedChunks.has(chunkIndex)) {
      chunkIndex++;
      continue;
    }
    
    currentUploads++;
    activeChunks++;
    
    const start = chunkIndex * CHUNK_SIZE;
    const end = Math.min(start + CHUNK_SIZE, file.size);
    const chunk = file.slice(start, end);
    
    uploadChunk(chunk, chunkIndex, FILE_ID, TOTAL_CHUNKS, file.name)
      .then(() => {
        uploadedChunks.add(chunkIndex);
        updateProgress(uploadedChunks.size, TOTAL_CHUNKS);
      })
      .catch(err => console.error(`分片${chunkIndex}失敗:`, err))
      .finally(() => {
        currentUploads--;
        activeChunks--;
      });
    
    chunkIndex++;
  }
  
  // 檢查所有分片完成
  const checkCompletion = setInterval(() => {
    if (activeChunks === 0 && uploadedChunks.size === TOTAL_CHUNKS) {
      clearInterval(checkCompletion);
      completeUpload(FILE_ID, file.name);
    }
  }, 1000);
}

async function uploadChunk(chunk, index, fileId, total, filename) {
  const formData = new FormData();
  formData.append('file', chunk, filename);
  formData.append('chunkIndex', index);
  formData.append('totalChunks', total);
  formData.append('fileId', fileId);
  
  return fetch('/api/upload/chunk', {
    method: 'POST',
    body: formData
  }).then(res => {
    if (!res.ok) throw new Error('上傳失敗');
    return res.json();
  });
}

async function completeUpload(fileId, filename) {
  return fetch('/api/upload/merge', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ fileId, filename })
  }).then(res => {
    if (res.ok) alert('上傳成功!');
    else alert('合并失敗');
  });
}

function updateProgress(done, total) {
  const percent = Math.round((done / total) * 100);
  document.getElementById('progressBar').innerHTML = `
    <div style="width: ${percent}%; background: #4CAF50; height: 20px;">
      ${percent}%
    </div>
  `;
}
</script>

2、SpringBoot后端實(shí)現(xiàn)

首先配置一下SpringBoot 上傳的一些限制

# application.yml
spring:
  servlet:
    multipart:
      max-file-size: 10MB       # 單片最大尺寸
      max-request-size: 1000MB  # 總請(qǐng)求限制
file:
  upload-dir: /data/upload

分片上傳控制器Controller

@RestController
@RequestMapping("/api/upload")
public class FileUploadController {
    
    @Value("${file.upload-dir}") //
    private String uploadDir;
    
    // 分片上傳接口
    @PostMapping("/chunk")
    public ResponseEntity<?> uploadChunk(
            @RequestParam("file") MultipartFile file,
            @RequestParam("chunkIndex") int chunkIndex,
            @RequestParam("totalChunks") int totalChunks,
            @RequestParam("fileId") String fileId) {
        
        try {
            // 創(chuàng)建分片存儲(chǔ)目錄
            String chunkDir = uploadDir + "/chunks/" + fileId;
            Path dirPath = Paths.get(chunkDir);
            if (!Files.exists(dirPath)) {
                Files.createDirectories(dirPath);
            }
            
            // 保存分片文件
            String chunkFilename = chunkIndex + ".part";
            Path filePath = dirPath.resolve(chunkFilename);
            Files.copy(file.getInputStream(), filePath, 
                StandardCopyOption.REPLACE_EXISTING);
            
            return ResponseEntity.ok().body(Map.of(
                "status", "success",
                "chunk", chunkIndex
            ));
        } catch (Exception e) {
            return ResponseEntity.status(500).body(Map.of(
                "status", "error",
                "message", e.getMessage()
            ));
        }
    }
    
    // 合并文件接口
    @PostMapping("/merge")
    public ResponseEntity<?> mergeChunks(
            @RequestBody MergeRequest request) {
        
        try {
            String fileId = request.getFileId();
            String filename = request.getFilename();
            
            Path chunkDir = Paths.get(uploadDir, "chunks", fileId);
            Path outputFile = Paths.get(uploadDir, filename);
            
            // 檢查分片完整性
            long expectedChunks = Files.list(chunkDir).count();
            if (expectedChunks != request.getTotalChunks()) {
                return ResponseEntity.badRequest().body(
                    "分片數(shù)量不匹配");
            }
            
            // 按序號(hào)排序分片
            List<Path> chunks = Files.list(chunkDir)
                .sorted((p1, p2) -> {
                    String f1 = p1.getFileName().toString();
                    String f2 = p2.getFileName().toString();
                    return Integer.compare(
                        Integer.parseInt(f1.split("\\.")[0]), 
                        Integer.parseInt(f2.split("\\.")[0]));
                })
                .collect(Collectors.toList());
            
            // 合并文件
            try (OutputStream out = Files.newOutputStream(outputFile, 
                StandardOpenOption.CREATE, StandardOpenOption.APPEND)) {
                
                for (Path chunk : chunks) {
                    Files.copy(chunk, out);
                }
            }
            
            // 清理分片目錄
            FileUtils.deleteDirectory(chunkDir.toFile());
            
            return ResponseEntity.ok().body(Map.of(
                "status", "success",
                "file", filename,
                "size", Files.size(outputFile)
            ));
        } catch (Exception e) {
            return ResponseEntity.status(500).body(
                "合并失敗: " + e.getMessage());
        }
    }
    
    // 請(qǐng)求體定義
    @Data
    public static class MergeRequest {
        private String fileId;
        private String filename;
        private int totalChunks;
    }
}

3、擴(kuò)展斷點(diǎn)續(xù)傳

如果你的項(xiàng)目沒(méi)有斷點(diǎn)續(xù)傳的需求,可以直接參考 ? ?前后端代碼即可,否則可以在分片上傳接口中添加續(xù)傳支持,增加代碼如下:

// 在分片上傳接口中添加續(xù)傳支持
@GetMapping("/check")
public ResponseEntity<?> checkChunks(
        @RequestParam("fileId") String fileId,
        @RequestParam("totalChunks") int totalChunks) {
    
    Path chunkDir = Paths.get(uploadDir, "chunks", fileId);
    if (!Files.exists(chunkDir)) {
        return ResponseEntity.ok().body(Map.of(
            "exists", false
        ));
    }
    
    try {
        // 獲取已上傳分片索引
        Set<Integer> uploaded = Files.list(chunkDir)
            .map(p -> Integer.parseInt(
                p.getFileName().toString().split("\\.")[0]))
            .collect(Collectors.toSet());
        
        return ResponseEntity.ok().body(Map.of(
            "exists", true,
            "uploaded", uploaded
        ));
    } catch (IOException e) {
        return ResponseEntity.status(500).body(
            "檢查失敗: " + e.getMessage());
    }
}

前端調(diào)用檢查接口:

async function checkUploadStatus(fileId, totalChunks) {
  const res = await fetch(`/api/upload/check?fileId=${fileId}&totalChunks=${totalChunks}`);
  const data = await res.json();
  return data.exists ? data.uploaded : new Set();
}

// 在上述前端代碼 startUpload函數(shù)中加入
const uploadedChunks = await checkUploadStatus(FILE_ID, TOTAL_CHUNKS);

5. 高級(jí)優(yōu)化方案

通過(guò)上面的代碼示例,你已經(jīng)可以輕松使用大文件的分片上傳了,如果你還有一些優(yōu)化需求,博主這里簡(jiǎn)單羅列三個(gè),供小伙伴們參考

5.1 分片秒傳優(yōu)化

// 在保存分片前計(jì)算哈希
String hash = DigestUtils.md5DigestAsHex(file.getBytes());
String chunkFilename = hash + ".part"; // 哈希作為文件名

// 檢查是否已存在相同分片
if (Files.exists(dirPath.resolve(chunkFilename))) {
    return ResponseEntity.ok().body(Map.of(
        "status", "skip",
        "chunk", chunkIndex
    ));
}

5.2 并行合并加速

// 使用并行流合并文件
List<Path> chunks = ... // 排序后的分片列表

try (OutputStream out = Files.newOutputStream(outputFile)) {
    chunks.parallelStream().forEach(chunk -> {
        try {
            Files.copy(chunk, out);
        } catch (IOException e) {
            throw new UncheckedIOException(e);
        }
    });
}

5.3 安全增強(qiáng)措施

// 文件名安全過(guò)濾
String safeFilename = filename.replaceAll("[^a-zA-Z0-9\\.\\-]", "_");

// 文件類型檢查
String mimeType = Files.probeContentType(filePath);
if (!mimeType.startsWith("video/")) {
    throw new SecurityException("非法文件類型");
}

結(jié)語(yǔ):構(gòu)建可靠的大文件傳輸體系

本文示例演示了一個(gè)從前端分片、并發(fā)上傳,到后端按序存儲(chǔ)與合并的完整流程。并可以按需提供斷點(diǎn)續(xù)傳,以及部分優(yōu)化的方案參考,這樣我們就提高大文件上傳的穩(wěn)定性與用戶體驗(yàn)。

通過(guò)本文實(shí)現(xiàn)的分片上傳方案,我們成功解決了大文件傳輸?shù)暮诵奶魬?zhàn):

  • 穩(wěn)定性提升:分片機(jī)制有效規(guī)避了網(wǎng)絡(luò)波動(dòng)影響
  • 資源優(yōu)化:內(nèi)存占用從GB級(jí)降至MB級(jí)
  • 用戶體驗(yàn):進(jìn)度可視化 + 斷點(diǎn)續(xù)傳
  • 擴(kuò)展能力:秒傳、并行合并等優(yōu)化空間

以上就是基于JavaScript+SpringBoot實(shí)現(xiàn)大文件分片上傳的詳細(xì)內(nèi)容,更多關(guān)于JavaScript SpringBoot文件分片上傳的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!

相關(guān)文章

最新評(píng)論