大文件上传如何做断点续传?(分别使用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){/*清理临时目录*/}
}
相关推荐
小白变怪兽7 分钟前
一、react18+项目初始化
前端·react.js·前端框架
我命由我1234512 分钟前
Android 开发问题:CardView 的阴影效果会受到父容器的裁切
android·java·开发语言·java-ee·android studio·android-studio·android runtime
日月星辰Ace24 分钟前
Java 中使用 Jackson 泛型反序列化时,为什么返回类型变成了 Object?——JavaType vs TypeReference 全解析
java
SimonKing37 分钟前
延迟消息的软肋,竟被定时任务完美弥补
java·后端·架构
天天摸鱼的java工程师37 分钟前
Spring Boot 3.0:开发效率直接起飞
java·后端
白瓷梅子汤38 分钟前
跟着官方示例学习 @tanStack-form --- Simple
前端·react.js
想躺平的咸鱼干1 小时前
用idea进行数据同步
java·ide·后端·elasticsearch·中间件·intellij-idea
vhgcc1 小时前
在 Java 中使用 Apache Tika 读取 doc、docx等格式文件内容
java·开发语言·自然语言处理·apache·ai编程
杨进军1 小时前
在React项目中利用 Symbol 防止 XSS 攻击的小技巧
react.js
inCBle1 小时前
vite+vue3+ts+electron桌面应用web端桌面端开发=>IPC进程通信!
前端·vue.js·electron