2025年最新!大文件上传全流程实战:分片、断点续传、秒传、并发控制一次讲透

有同学问到相关的问题,抽空简单回答一下

在现代 Web 应用中,大文件上传早已不是「锦上添花」的功能,而是电商、视频平台、企业网盘等产品的基础能力 。尤其在动辄数百 MB 或数 GB 的文件场景下,直传方式几乎不可用 ------ 超时、内存溢出、网络中断,统统会让用户体验崩溃。

本文将带你实现一个工业级大文件上传方案 ,覆盖 分片上传、断点续传、秒传、并发控制、错误重试、手动中断 等核心功能,并进一步扩展到上传进度可视化、服务端安全校验、云存储对接等更贴近生产的场景。


一、目标拆解:我们要解决什么问题?

对照真实业务,我们需要的功能清单如下:

  • 分片上传:避免超时,突破请求大小限制
  • 秒传:服务端已存在文件 → 直接返回成功
  • 断点续传:上传中断后,仅补传缺失分片
  • 并发控制:避免同时发起过多请求压垮浏览器/服务端
  • 错误重试:网络波动时自动重试几次,减少用户干预
  • 手动中断:用户可随时暂停/取消上传
  • 上传进度可视化:进度条+速率提示,增强体验
  • 云存储对接:与 S3/OSS/MinIO 等无缝兼容
  • 安全与校验:防止恶意上传 & 文件完整性保证

二、上传全流程拆解

大文件上传可抽象为 6 个阶段

  1. 文件选择(File API 获取文件对象)
  2. 分片 + 哈希计算(生成唯一标识)
  3. 文件校验(秒传 / 断点续传判断)
  4. 分片上传(含并发控制、错误重试、中断机制)
  5. 合并分片(服务端流式拼接)
  6. 上传完成校验(文件完整性验证、存储持久化)

接下来分前后端分别展开。


三、前端实现(Vue 3)

1. 文件选择与状态管理

ini 复制代码
<template>
  <div class="upload-container">
    <input type="file" @change="handleFileSelect" />
    <button v-if="isUploading" @click="abortUpload">中断上传</button>
    <div v-if="progress > 0">
      上传进度:{{ progress }}%
    </div>
  </div>
</template>

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

const isUploading = ref(false);
const progress = ref(0);
const abortControllers = ref([]);
const fileInfo = ref({});
</script>

2. 分片与哈希优化

和之前一样,我们按 1MB 分片,但要注意:

  • 大文件(>500MB)建议 5MB/10MB,减少请求数
  • 抽样 + 全量混合计算哈希,保证唯一性与速度
ini 复制代码
import SparkMD5 from "spark-md5";

const CHUNK_SIZE = 5 * 1024 * 1024;

function createChunks(file) {
  const chunks = [];
  let cur = 0;
  while (cur < file.size) {
    chunks.push(file.slice(cur, cur + CHUNK_SIZE));
    cur += CHUNK_SIZE;
  }
  return chunks;
}

async function calHash(chunks) {
  const spark = new SparkMD5.ArrayBuffer();
  const reader = new FileReader();
  const targets = [];

  chunks.forEach((chunk, i) => {
    if (i === 0 || i === chunks.length - 1) {
      targets.push(chunk);
    } else {
      targets.push(chunk.slice(0, 2));
      targets.push(chunk.slice(CHUNK_SIZE / 2, CHUNK_SIZE / 2 + 2));
      targets.push(chunk.slice(CHUNK_SIZE - 2, CHUNK_SIZE));
    }
  });

  return new Promise((resolve) => {
    reader.readAsArrayBuffer(new Blob(targets));
    reader.onload = (e) => {
      spark.append(e.target.result);
      resolve(spark.end());
    };
  });
}

3. 校验文件状态(秒传/断点续传)

javascript 复制代码
async function verifyFile(fileHash, fileName) {
  const res = await fetch("http://localhost:3000/verify", {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({ fileHash, fileName }),
  });
  return res.json();
}

4. 分片上传(并发 + 重试 + 中断)

这是前端最复杂的部分:

  • 并发池:限制并发数(如 6 个)
  • 错误重试:失败的分片可重试 3 次
  • 中断机制 :统一用 AbortController
ini 复制代码
async function uploadWithPool(formDatas, retries = 3) {
  const MAX_CONCURRENT = 6;
  let index = 0;
  const pool = [];

  async function request(formData, attempt = 1) {
    const controller = new AbortController();
    abortControllers.value.push(controller);

    try {
      await fetch("http://localhost:3000/upload", {
        method: "POST",
        body: formData,
        signal: controller.signal,
      });
    } catch (err) {
      if (attempt < retries && err.name !== "AbortError") {
        return request(formData, attempt + 1); // 自动重试
      }
      throw err;
    }
  }

  while (index < formDatas.length) {
    const task = request(formDatas[index]);
    pool.push(task);

    if (pool.length >= MAX_CONCURRENT) {
      await Promise.race(pool);
      pool.splice(pool.indexOf(await Promise.race(pool)), 1);
    }
    index++;
  }

  await Promise.all(pool);
}

5. 上传进度条

利用 XMLHttpRequestfetch + stream,实时监听上传进度。

ini 复制代码
function uploadChunk(formData) {
  return new Promise((resolve, reject) => {
    const xhr = new XMLHttpRequest();
    xhr.open("POST", "http://localhost:3000/upload");
    xhr.upload.onprogress = (e) => {
      if (e.lengthComputable) {
        progress.value = Math.round((e.loaded / e.total) * 100);
      }
    };
    xhr.onload = () => resolve();
    xhr.onerror = () => reject("网络错误");
    xhr.send(formData);
  });
}

四、后端实现(Express)

1. 文件校验(/verify)

ini 复制代码
app.post("/verify", async (req, res) => {
  const { fileHash, fileName } = req.body;
  const filePath = path.resolve(UPLOAD_DIR, `${fileHash}${extractExt(fileName)}`);

  if (fse.existsSync(filePath)) {
    return res.json({ shouldUpload: false });
  }

  const chunkDir = path.resolve(UPLOAD_DIR, fileHash);
  const existChunks = fse.existsSync(chunkDir) ? await fse.readdir(chunkDir) : [];
  res.json({ shouldUpload: true, existChunks });
});

2. 分片接收(/upload)

ini 复制代码
app.post("/upload", (req, res) => {
  const form = new multiparty.Form();
  form.parse(req, async (err, fields, files) => {
    const fileHash = fields.filehash[0];
    const chunkHash = fields.chunkhash[0];
    const chunkDir = path.resolve(UPLOAD_DIR, fileHash);
    await fse.ensureDir(chunkDir);
    await fse.move(files.chunk[0].path, path.resolve(chunkDir, chunkHash));
    res.json({ status: true });
  });
});

3. 合并分片(/merge)

ini 复制代码
app.post("/merge", async (req, res) => {
  const { fileHash, fileName, size } = req.body;
  const filePath = path.resolve(UPLOAD_DIR, `${fileHash}${extractExt(fileName)}`);
  const chunkDir = path.resolve(UPLOAD_DIR, fileHash);

  const chunkFiles = await fse.readdir(chunkDir);
  chunkFiles.sort((a, b) => parseInt(a.split("-")[1]) - parseInt(b.split("-")[1]));

  for (let i = 0; i < chunkFiles.length; i++) {
    const chunkPath = path.resolve(chunkDir, chunkFiles[i]);
    await pipeStream(chunkPath, fse.createWriteStream(filePath, { start: i * size }));
    await fse.unlink(chunkPath);
  }

  await fse.remove(chunkDir);
  res.json({ status: true, message: "合并完成" });
});

function pipeStream(path, writeStream) {
  return new Promise((resolve) => {
    const readStream = fse.createReadStream(path);
    readStream.on("end", resolve);
    readStream.pipe(writeStream);
  });
}

五、工程化可扩展方向

  1. 进度持久化:将分片完成状态存入 localStorage,刷新后可继续上传。
  2. 云存储对接:后端仅签发预签名 URL,分片直传 S3/OSS,减少服务端压力。
  3. 安全校验:限制文件类型、大小;在合并时校验哈希一致性。
  4. 大规模优化:上传日志采集,失败自动上报告警。
  5. 秒传优化 :可在数据库维护 fileHash → 文件路径 映射。

六、总结

  • 分片 → 校验 → 上传 → 合并 → 校验 是大文件上传的黄金流程
  • 并发池、错误重试、进度条 是提升用户体验的关键
  • 云直传 + 安全校验 是生产环境必须的扩展

大文件上传从「玩具版」到「工程级」,关键就在于:细节补偿 + 可扩展性设计

相关推荐
晴空雨10 分钟前
💥 React 容器组件深度解析:从 Props 拦截到事件改写
前端·react.js·设计模式
Marshall357215 分钟前
前端水印防篡改原理及实现
前端
阿虎儿27 分钟前
TypeScript 内置工具类型完全指南
前端·javascript·typescript
IT_陈寒35 分钟前
Java性能优化实战:5个立竿见影的技巧让你的应用提速50%
前端·人工智能·后端
chxii1 小时前
6.3Element UI 的表单
javascript·vue.js·elementui
张努力1 小时前
从零开始的开发一个vite插件:一个程序员的"意外"之旅 🚀
前端·vue.js
远帆L1 小时前
前端批量导入内容——word模板方案实现
前端
Codebee1 小时前
OneCode3.0-RAD 可视化设计器 配置手册
前端·低代码
深兰科技1 小时前
深兰科技:搬迁公告,我们搬家了
javascript·人工智能·python·科技·typescript·laravel·深兰科技