大文件上传如何做断点续传?(分别使用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){/*清理临时目录*/}
}
相关推荐
Lhappy嘻嘻8 小时前
Java IO|File 文件操作 + 字节流 / 字符流完整笔记 + 递归删除文件实战
java·笔记·php
伊玛目的门徒8 小时前
试用leetcode之典中典 二数之和问题
java·算法·leetcode
懒鸟一枚11 小时前
深入理解 Linux 内存、Swap 交换分区与分页机制的关系
java·linux·数据库
我命由我1234513 小时前
执行 Gradle 指令报错,无法将“grep”项识别为 cmdlet、函数、脚本文件或可运行程序的名称
android·java·java-ee·android studio·android jetpack·android-studio·android runtime
考虑考虑13 小时前
Sentinel安装
java·后端·微服务
碎碎念_49213 小时前
SpringBoot + Vue 前后端分离从 0 到 1 完整环境配置流程
vue.js·spring boot·后端
shuaijie051814 小时前
强制修改调用接口的api地址。
javascript·vue.js·ecmascript
凤凰院凶涛QAQ14 小时前
《Java版数据结构 & 集合类剖析》栈与队列:“push/pop 是栈的灵魂,offer/poll 是队列的骨架——四组 API,两种人生”
java·开发语言·数据结构
光影少年15 小时前
RN 样式特点:Flex 布局默认、没有像素单位、样式不能级联
react native·react.js·掘金·金石计划
掘金_答案15 小时前
上线那天,一个 ConcurrentHashMap 差点送走我的 AI 客服——3 天排查 JVM 血泪史
java·后端·架构