大文件分片上传实战:断点续传与秒传功能的前端实现方案

大文件上传是前端绕不开的工程问题。浏览器直接提交500MB以上文件,一旦网络抖动就得从零重来,而HTTP请求体上限、网关超时、内存占用三道坎也绕不过去。分片上传把文件切成小块逐个提交,配合断点续传与文件秒传,能把失败重传成本降到最低。这一套方案的核心是三个模块:文件指纹计算、分片队列控制、续传状态同步。

文件哈希指纹计算与秒传判断逻辑

秒传的前提是给每个文件算出唯一指纹。全量读取一个2GB文件计算MD5在普通电脑上需要十几秒且内存吃紧,生产方案用SparkMD5对ArrayBuffer增量计算,配合抽样哈希把计算时间压到秒级。抽样哈希只取文件首尾各2MB加中间每隔一定步长取一段,冲突概率对业务场景足够:

async function calcFileHash(file) {
  // 抽样:首2MB + 尾2MB + 中间每隔 chunkSize*2 取一块
  const CHUNK_SIZE = 5 * 1024 * 1024; // 5MB一片
  const chunks = Math.ceil(file.size / CHUNK_SIZE);
  const samples = [];
  // 首片
  samples.push(file.slice(0, 2 * 1024 * 1024));
  // 中间抽样片
  for (let i = 2; i < chunks - 2; i += 2) {
    samples.push(file.slice(i * CHUNK_SIZE, i * CHUNK_SIZE + 64 * 1024));
  }
  // 尾片
  if (chunks > 2) {
    samples.push(file.slice(file.size - 2 * 1024 * 1024));
  }
  const spark = new SparkMD5.ArrayBuffer();
  for (const blob of samples) {
    spark.append(await blob.arrayBuffer());
  }
  return spark.end();
}

上传前先把指纹发给服务端查询:若服务端已存在相同指纹的完整文件,直接标记上传成功,整个文件一个字节都不用传,这就是秒传。若服务端返回已完成的分片列表,前端据此跳过这些分片,从断点继续,这就是断点续传。两者共用一个接口:

async function checkFileExist(hash, filename, totalChunks) {
  const res = await fetch('/api/upload/check', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ hash, filename, totalChunks })
  });
  return res.json(); // { exists: true } 或 { uploadedChunks: [0,2,3,7] }
}

Axios并发队列控制与分片上传核心代码

分片切片用Blob.slice完成,注意slice不产生真实拷贝,内存开销可控。上传环节的关键是并发控制,10GB文件切成2000个5MB分片,无脑全发会把浏览器并发上限和服务器连接数同时打爆。实现一个固定窗口并发池:

async function uploadChunks(file, hash, uploadedChunks, onProgress) {
  const CHUNK_SIZE = 5 * 1024 * 1024;
  const total = Math.ceil(file.size / CHUNK_SIZE);
  const tasks = [];
  for (let i = 0; i < total; i++) {
    if (uploadedChunks.includes(i)) continue; // 断点续传:跳过已完成分片
    tasks.push({
      index: i,
      blob: file.slice(i * CHUNK_SIZE, (i + 1) * CHUNK_SIZE)
    });
  }

  const CONCURRENCY = 4; // 并发上限
  let done = uploadedChunks.length;
  let failList = [];

  async function worker(queue) {
    while (queue.length > 0) {
      const item = queue.shift();
      const fd = new FormData();
      fd.append('file', item.blob);
      fd.append('hash', hash);
      fd.append('index', item.index);
      fd.append('total', total);
      try {
        await axios.post('/api/upload/chunk', fd, { timeout: 30000 });
        done++;
        onProgress(Math.round(done / total * 100));
      } catch (e) {
        failList.push(item); // 失败分片进入重试队列
      }
    }
  }

  // 4个worker共享同一队列
  await Promise.all(
    Array.from({ length: CONCURRENCY }, () => worker([...tasks]))
  );

  // 失败分片重试一轮
  if (failList.length) {
    await Promise.all(Array.from({ length: 2 }, () => worker(failList)));
  }
  return { uploaded: done, failed: failList.length };
}

worker函数从共享队列取任务执行,天然实现固定窗口并发。失败分片收集后单独重试,比每个请求各自retry更可控。服务端收到全部分片后按index顺序写入临时目录,校验总大小后合并成完整文件,并将哈希入库供下次秒传比对。

分片进度显示与页面刷新后的续传状态保持

进度条是用户体验的分水岭。用已上传分片数除以总数计算比例,比单纯统计字节数更准确,因为分片是原子操作,不存在传一半的模糊态。页面刷新场景下续传不中断,靠的是服务端check接口返回的uploadedChunks,用户重新选择同一文件后,前端用文件指纹查询服务端进度,直接从断点续传。Vue3组件示例:

<template>
  <div class="uploader">
    <input type="file" @change="onFileChange" />
    <div v-if="progress > 0" class="progress">
      <div class="bar" :style="{ width: progress + '%' }"></div>
      <span>{{ progress }}%</span>
      <span v-if="instantUpload">(秒传成功)</span>
    </div>
  </div>
</template>

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

const progress = ref(0);
const instantUpload = ref(false);

async function onFileChange(e) {
  const file = e.target.files[0];
  const hash = await calcFileHash(file);
  const total = Math.ceil(file.size / 5 * 1024 * 1024);
  const { exists, uploadedChunks } = await checkFileExist(hash, file.name, total);
  if (exists) {
    progress.value = 100;
    instantUpload.value = true;
    return; // 秒传:服务端已有完整文件
  }
  await uploadChunks(file, hash, uploadedChunks, (p) => progress.value = p);
  await fetch('/api/upload/merge', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ hash, filename: file.name, total })
  });
}
</script>

几个工程细节:文件超过1GB时抽样哈希的计算要放到Web Worker里跑,避免主线程卡死;分片大小按目标用户带宽取值,5MB在4G网络下单分片约2秒,超时重试粒度合适,内网场景可以放大到20MB减少请求数;hash加上文件最后修改时间戳参与计算可以防止内容相同但需重新上传的场景误判秒传;上传完成的merge请求要设计幂等,用户重复点击合并按钮不能产生重复文件。

原创文章,作者:小编,如若转载,请注明出处:https://www.yunthe.com/da-wen-jian-fen-pian-shang-chuan-shi-zhan-duan-dian-xu/

(0)
小编小编
上一篇 50分钟前
下一篇 50分钟前

相关推荐