大文件上传如何做断点续传?(分别使用vue、React、java)

大文件上传断点续传的实现逻辑

大文件上传断点续传的核心在于将文件分成多个小片段(分片),逐个上传这些片段到服务器,并记录已成功上传的部分。如果上传中断,可以基于之前保存的状态重新恢复未完成部分的上传过程。

实现逻辑的关键点
  1. 前端处理

    使用浏览器提供的 File APIBlob.slice() 方法来切割文件成若干个小块。每一块可以通过 HTTP 请求发送至服务器。

  2. 唯一标识符

    为了区分不同用户的上传请求以及同一文件的不同分片,通常会为每次上传生成唯一的标识符(UUID 或 MD5 值)。该标识符用于标记当前上传的任务状态。

  3. 进度跟踪与存储

    客户端通过本地缓存机制(如 IndexedDB、LocalStorage)或 Cookies 来记录已完成的分片编号;而服务端则维护一份全局状态表,用来验证哪些分片已经收到并持久化下来。

  4. 错误重试机制

    如果某个分片失败,则允许客户端自动尝试再次提交直到成功为止。这一步骤可通过设置定时器配合 Promise 链式调用来达成目标效果。

  5. 合并操作

    当所有分片都到达服务器之后,由后台程序负责按照顺序拼接回原始数据流形式最后写入磁盘成为完整的文档副本。


Vue3 中的具体实现示例

以下是基于 Vue3 的简单版代码框架:

javascript 复制代码
<template>
  <div>
    <input type="file" @change="handleFileChange"/>
    <button @click="uploadChunks">开始上传</button>
  </div>
</template>

<script setup>
import { ref } from 'vue';

const file = ref(null);
const chunkSize = 1 * 1024 * 1024; // 每次上传大小设为1M字节

function handleFileChange(event) {
  file.value = event.target.files[0];
}

async function uploadChunks() {
  const chunks = [];
  let currentChunk = 0;

  while (currentChunk * chunkSize < file.value.size) {
    const start = currentChunk * chunkSize;
    const end = Math.min(file.value.size, start + chunkSize);

    chunks.push({
      blob: file.value.slice(start, end),
      index: currentChunk,
    });

    currentChunk++;
  }

  for (let i = 0; i < chunks.length; i++) {
    try {
      await fetch('/api/upload', {
        method: 'POST',
        headers: { 'Content-Type': 'application/octet-stream' },
        body: chunks[i].blob,
      });
      console.log(`第${i}块上传完毕`);
    } catch (error) {
      console.error('上传失败:', error.message);
    }
  }
}
</script>

React 中的具体实现示例

下面是一个利用 Hooks 构建的大致流程展示:

jsx 复制代码
import React, { useState } from 'react';

function App() {
  const [selectedFile, setSelectedFile] = useState(null);

  const handleChange = (event) => {
    setSelectedFile(event.target.files[0]);
  };

  const handleSubmit = async () => {
    if (!selectedFile) return alert('请选择要上传的文件');

    const CHUNK_SIZE = 1 * 1024 * 1024; // 设置每个chunk为1MB
    const totalChunks = Math.ceil(selectedFile.size / CHUNK_SIZE);

    for (let i = 0; i < totalChunks; ++i) {
      const start = i * CHUNK_SIZE;
      const end = ((i + 1) * CHUNK_SIZE > selectedFile.size ? selectedFile.size : (i + 1) * CHUNK_SIZE);

      const formData = new FormData();
      formData.append('file', selectedFile.slice(start, end));
      formData.append('index', i.toString());

      try {
        const response = await fetch("/upload", {
          method: "POST",
          body: formData,
        });

        if (!response.ok) throw Error(response.statusText);
      } catch (err) {
        console.warn(err);
      }
    }
  };

  return (
    <>
      <input type="file" onChange={handleChange}/>
      <button onClick={handleSubmit}>Upload File</button>
    </>
  );
}

export default App;

Java Spring Boot 后端接收示例

Spring Boot 提供了强大的 RESTful 支持能力,在这里我们定义了一个简单的接口用以接受来自前端传递过来的数据包:

java 复制代码
@RestController
@RequestMapping("/upload")
public class UploadController {

    private static final String UPLOAD_DIR = "/tmp/uploads/";

    @PostMapping(consumes = MediaType.APPLICATION_OCTET_STREAM_VALUE)
    public ResponseEntity<String> handleFilePart(@RequestBody byte[] bytes,
                                               @RequestParam Integer index,
                                               HttpServletRequest request) throws IOException {
        
        UUID taskId = extractTaskIdFromRequest(request); 

        Path targetLocation = Paths.get(UPLOAD_DIR).resolve(taskId.toString()).resolve(index + ".part");
        Files.write(targetLocation, bytes);

        return ResponseEntity.status(HttpStatus.CREATED).body("Uploaded part:" + index);
    }

    private UUID extractTaskIdFromRequest(HttpServletRequest req){
       /* 解析header或者其他参数获取task id */
       return null;
    }
    
    @GetMapping("/{taskId}")
    public void mergeParts(@PathVariable String taskId)throws Exception{
         List<Path> parts=Files.walk(Paths.get(UPLOAD_DIR))
             .filter(p->p.toAbsolutePath().toString().contains(taskId+"\\"))
             .sorted()
             .collect(Collectors.toList());
         
         RandomAccessFile raf=new RandomAccessFile(new File(UPLOAD_DIR+File.separator+taskId+".final"), "rw");

         for(Path p:parts){
            FileInputStream fis=new FileInputStream(p.toString());
            byte [] buffer=new byte[(int)p.toFile().length()];
            fis.read(buffer);
            raf.write(buffer);
            fis.close();            
         }
         raf.close();

         deleteTempFolderIfNecessary(parts.stream().map(Path::toFile).toArray(File[]::new));      
     }
     
     private void deleteTempFolderIfNecessary(File...filesToDelete){/*清理临时目录*/}
}
相关推荐
钢铁男儿15 分钟前
C# 接口(什么是接口)
java·数据库·c#
丶小鱼丶29 分钟前
排序算法之【归并排序】
java·排序算法
上上迁31 分钟前
分布式生成 ID 策略的演进和最佳实践,含springBoot 实现(Java版本)
java·spring boot·分布式
永日4567031 分钟前
学习日记-spring-day42-7.7
java·学习·spring
骑自行车的码农32 分钟前
React短文系列 遍历fiber树 App的创建
前端·react.js
斯~内克41 分钟前
基于Vue.js和PDF-Lib的条形码生成与批量打印方案
前端·vue.js·pdf
龙谷情Sinoam1 小时前
扩展若依@Excel注解,使其对字段的控制是否导出更加便捷
java
二十雨辰1 小时前
[尚庭公寓]07-Knife快速入门
java·开发语言·spring
sunbyte1 小时前
50天50个小项目 (Vue3 + Tailwindcss V4) ✨ | ContentPlaceholder(背景占位)
前端·javascript·css·vue.js·tailwindcss
爱学习的茄子1 小时前
React Hooks进阶:从0到1打造高性能Todo应用
前端·react.js·面试