前端大文件上传全攻略(秒传、分片上传、断点续传、并发控制、进度展示)

前端大文件上传全攻略(秒传、分片上传、断点续传、并发控制、进度展示)

在一次面试中,我被问到"有没有做过大文件上传"。虽然实际项目中没有遇到,但这是一个前端高频面试题,涉及文件处理、并发控制、性能优化等多个知识点。

为了更系统地理解和展示专业性,我专门实现并总结了大文件上传的常见功能:

  • 秒传(快速上传)
  • 分片上传(Chunk Upload)
  • 断点续传
  • 并发控制
  • 上传进度 & UI 展示

1、秒传(快速上传)

为什么需要秒传?

如果服务端已经存在相同文件,再次上传会造成存储浪费和重复操作 。解决办法是:利用文件 Hash 值作为唯一标识,判断文件是否存在。

实现思路

  1. 前端计算文件 Hash;
  2. 将 Hash 传给服务端;
  3. 服务端比对:存在 → 秒传成功;不存在 → 进入分片上传。

Hash 计算方式

  • spark-md5:轻量、适合小文件。
  • hash-wasm:基于WASM,支持多种算法(MD5、SHA-1、SHA-256等),对大文件更高效。

问题 :大文件Hash计算耗时,会造成页面卡顿。

解决方案

  • 切片 + Web Worker :文件切片后在Worker中计算,避免阻塞主线程;
  • 抽样 Hash:只取部分内容计算,性能好,但准确性略差。

代码

tsx 复制代码
// 秒传逻辑
const handleSecondUpload = async (file: File) => {
  const start = performance.now(); // ⏱ 开始计时
  const fileHash = await calculateHash(file);
  const end = performance.now(); // ⏱ 结束计时
  console.log('duration', end - start); // 计算hash 耗时
  if (!fileHash) return;
  // 请求接口把计算后的文件Hash传递给后端
  const checkRes = await fetch('/api/checkFile', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ fileHash })
  })
  const { exist, uploadedIndexes } = await checkRes.json();
  if (exist) {
    console.log('文件已存在,秒传成功');
    setProgress(100);
    return;
  }
}
tsx 复制代码
// 生成文件 hash
const calculateHash = (file: File, chunkSize = 2 * 1024 * 1024) => {
  return new Promise<string>((resolve, reject) => {
    // 在 web worker 中计算Hash
    const worker = new Worker(new URL('@/utils/hashWorker.ts', import.meta.url), {
      type: 'module' // ✅ 指定为模块 Worker
    });
    worker.postMessage({ file, chunkSize: 2 * 1024 * 1024 }); // 2MB 一片
    worker.onmessage = e => {
      const { progress, hash } = e.data;
      if (progress) {
        console.log(`计算进度: ${progress.toFixed(2)}%`);
        setProgress(progress.toFixed(2));
      }
      if (hash) {
        console.log('计算完成,文件 hash:', hash);
        resolve(hash);
        worker.terminate();
      }
    };
  });
};
ts 复制代码
// hashWorker.ts  --> 计算hash
import { createMD5 } from 'hash-wasm'; // 基于 WASM 实现
self.onmessage = async (e: any) => {
  const { file, chunkSize } = e.data;
  const chunk = chunkSize || 2 * 1024 * 1024; // 默认 2MB 分片
  let offset = 0;
  // 创建 hash-wasm 的 MD5 实例
  const md5 = await createMD5();
  const reader = new FileReader();
  // 读取切片
  const loadNext = () => {
    const slice = file.slice(offset, offset + chunk);
    reader.readAsArrayBuffer(slice);
  };

  reader.onload = (event: any) => {
    md5.update(new Uint8Array(event.target.result)); // 更新 hash
    offset += chunk;

    // 发送进度
    self.postMessage({ progress: Math.min(100, (offset / file.size) * 100) });

    if (offset < file.size) {
      loadNext(); // 读取下一片
    } else {
      // 计算完成,返回 hash
      const hash = md5.digest(); // 默认输出 hex
      self.postMessage({ hash });
    }
  };

  // 开始读取第一片
  loadNext();
};

2、分片上传(Chunk Upload)

为什么需要分片?

浏览器和网络对单次请求大小有限制,文件过大容易失败。

解决方法:将文件切成小块(例如 5MB/片),逐个上传。

代码:

tsx 复制代码
// 进行文件切片
interface Chunk {
  index: number;
  data: Blob;
}
const CHUNK_SIZE = 5 * 1024 * 1024; // 每个分片 5MB
const handleChunkSlice = (file: File) => {
  const chunks: Chunk[] = [];
  let cur = 0;
  let index = 0;
  while (cur < file.size) {
    chunks.push({ index, data: file.slice(cur, cur + CHUNK_SIZE) });
    cur += CHUNK_SIZE;
    index++;
  }
  return chunks;
};

这里切片之前其实还有一步,请求后端接口,获取已经上传的切片,假如后端返回了已经上传切片索引

tsx 复制代码
// 后端返回的已经上传片段索引
const uploadedIndexes = [0, 1, 3, 4, 7, 9, 10, 12, 15, 23, 35];

3、断点续传

为什么需要断点续传?

上传中途失败时,如果每次都要从头开始,体验极差。

解决办法:记录已上传的分片索引,下次只传未完成部分。

代码:

tsx 复制代码
let completed = 0; // 上传进度
let currentIndex = 0; // 当前上传到哪一片
const uploadedIndexes = [0, 1, 3, 4, 7, 9, 10, 12, 15, 23, 35]; // 已经上传过的切片
const uploadNext = async (chunks: Chunk[]) => {
  if (currentIndex >= chunks.length) return;
  const chunk = chunks[currentIndex];
  currentIndex++;
  // 断点续传:如果这个分片已上传,直接跳过
  if (uploadedIndexes?.includes(chunk.index)) {
    console.log(`跳过已上传分片 ${chunk.index}`);
    completed++;
    setProgress(Math.round((completed / chunks.length) * 100));
    return uploadNext(); // 继续下一个
  }
  try {
    await uploadChunk(chunk, fileHash); // 调用真实接口上传分片
    completed++;
    setProgress(Math.round((completed / chunks.length) * 100));
  } catch (error) {
    console.error(`分片 ${chunk.index} 上传失败`, error);
    // 可在这里实现重试逻辑,
  }
  // 递归上传下一个
  await uploadNext();
};
tsx 复制代码
/**
 * @description 上传切片
 * @param {string} fileHash 上传文件hash,使用文件名的话,文件名可能修改
 */
const uploadChunk = (chunk: Chunk[], fileHash: sting) => {
  if (!fileHash) return;
  const formData = new FormData();
  formData.append('file', chunk); // 分片内容
  formData.append('index', String(chunk.index)); // 分片索引
  formData.append('fileHash', fileHash); // 原文件名
  const res = await fetch('/uploadChunk', {
    method: 'POST',
    body: formData
  });
  if (!res.ok) throw new Error(`分片 ${chunk.index} 上传失败`);
  console.log(`分片 ${chunk.index} 上传成功`);
};

4、并发控制

为什么需要并发控制?

一次性发出过多请求会导致:

  • 浏览器报错(并发过高);
  • 服务端压力过大。

解决方案 :限制并发数量,例如同时只上传 3 个分片。根据循环会生成 3 条promise链,然后uploadNext内通过递归不断发起请求。每条链的请求只有等上一次请求结束后,才能继续下一次请求。所以总的并发量就是 3 个。

代码:

tsx 复制代码
const maxConcurrency = 3; // 并发数量,可调整
// 初始化并发链
for (let i = 0; i < Math.min(maxConcurrency, chunks.length); i++) {
  promises.push(uploadNext());
}
await Promise.all(promises);

5、上传进度 & UI 展示

上传进度计算公式:

matlab 复制代码
进度 = (已完成分片数 ÷ 总分片数) × 100%

需要考虑的情况:

  • 秒传直接完成;
  • 断点续传跳过分片;
  • 并发上传同时完成多个分片。

前端可以使用进度条(Progress Bar)或百分比文字来实时展示。

相关推荐
跟橙姐学代码2 小时前
Python 类的正确打开方式:从新手到进阶的第一步
前端·python·ipython
Jagger_2 小时前
SonarQube:提升代码质量的前后端解决方案
前端·后端·ai编程
Becauseofyou1372 小时前
如果你刚入门Three.js,这几个开源项目值得你去学习
前端·three.js
不一样的少年_2 小时前
同事以为要重写,我8行代码让 Vue 2 公共组件跑进 Vue 3
前端·javascript·vue.js
云枫晖2 小时前
JS核心知识-数据转换
前端·javascript
xuyanzhuqing2 小时前
代码共享方案-多仓库合并单仓库
前端
RaidenLiu2 小时前
Riverpod 3:重建控制的实践与性能优化指南
前端·flutter
学习中的小胖子2 小时前
React的闭包陷阱
前端
不卷的攻城狮2 小时前
【精通react】(五)react 函数时组件为什么需要 hooks?
前端