一篇文章彻底理解大文件分片上传、秒传、断点续传等功能

2,063 阅读3分钟

前言

在大文件上传场景中,传统的一次性上传方式会面临网络不稳定、内存占用高、服务器压力大等问题。本文将介绍如何通过分片上传断点续传秒传技术优化用户体验,并附上完整代码实现。


一、核心功能与需求分析

1. 分片上传

  • 目标:将大文件切割为多个小分片,分批次上传。
  • 优势:降低单次请求压力,提升上传稳定性。

2. 断点续传

  • 目标:上传中断后,可从断点处继续上传。
  • 实现:记录已上传的分片信息,跳过已传分片。

3. 秒传

  • 目标:服务器已存在相同文件时,直接返回结果,无需重复上传。
  • 实现:通过文件哈希(如MD5)验证文件唯一性。

二、实现原理

1. 分片上传流程

1. 用户选择文件 → 2. 计算文件哈希 → 3. 文件分片 → 4. 上传分片 → 5. 合并分片

2. 技术依赖

  • 前端File APISparkMD5(计算哈希)、Axios(HTTP请求)
  • 后端:分片存储、合并接口、哈希校验接口

三、代码实现

1. HTML结构

<input type="file" id="fileInput" />
<button onclick="upload()">开始上传</button>
<div id="progress"></div>

2. 计算文件哈希(秒传关键)

import SparkMD5 from 'spark-md5';

// 计算文件哈希(Web Worker优化)
async function calculateHash(file) {
  return new Promise((resolve) => {
    const spark = new SparkMD5.ArrayBuffer();
    const reader = new FileReader();
    const chunkSize = 2 * 1024 * 1024; // 2MB分片计算哈希
    let currentChunk = 0;

    reader.onload = (e) => {
      spark.append(e.target.result);
      currentChunk++;
      if (currentChunk < Math.ceil(file.size / chunkSize)) {
        loadNext();
      } else {
        resolve(spark.end());
      }
    };

    function loadNext() {
      const start = currentChunk * chunkSize;
      const end = start + chunkSize >= file.size ? file.size : start + chunkSize;
      reader.readAsArrayBuffer(file.slice(start, end));
    }

    loadNext();
  });
}

3. 分片上传逻辑

async function upload() {
  const file = document.getElementById('fileInput').files[0];
  if (!file) return;

  // 1. 计算文件哈希(用于秒传)
  const fileHash = await calculateHash(file);
  
  // 2. 检查是否已存在文件(秒传)
  const { data: existData } = await axios.post('/api/check', { fileHash });
  if (existData.exist) {
    alert('秒传成功!');
    return;
  }

  // 3. 分片上传
  const chunkSize = 5 * 1024 * 1024; // 5MB分片
  const chunkCount = Math.ceil(file.size / chunkSize);
  const uploadedChunks = existData.uploadedChunks || [];

  for (let i = 0; i < chunkCount; i++) {
    // 跳过已上传的分片(断点续传)
    if (uploadedChunks.includes(i)) continue;

    const start = i * chunkSize;
    const end = Math.min(start + chunkSize, file.size);
    const chunk = file.slice(start, end);

    const formData = new FormData();
    formData.append('chunk', chunk);
    formData.append('hash', fileHash);
    formData.append('index', i);

    await axios.post('/api/upload', formData, {
      onUploadProgress: (e) => {
        const progress = ((i * chunkSize + e.loaded) / file.size * 100).toFixed(2);
        document.getElementById('progress').innerText = `上传进度:${progress}%`;
      },
    });
  }

  // 4. 通知合并分片
  await axios.post('/api/merge', { fileHash, fileName: file.name });
  alert('上传成功!');
}

四、后端关键接口设计(Node.js示例)

1. 检查文件是否已存在(秒传)

app.post('/api/check', (req, res) => {
  const { fileHash } = req.body;
  const filePath = path.resolve(__dirname, 'uploads', `${fileHash}.merge`);

  // 检查文件是否存在
  if (fs.existsSync(filePath)) {
    return res.json({ exist: true });
  }

  // 检查已上传的分片(断点续传)
  const chunkDir = path.resolve(__dirname, 'uploads', fileHash);
  const uploadedChunks = fs.existsSync(chunkDir) 
    ? fs.readdirSync(chunkDir).map(Number) 
    : [];
  
  res.json({ exist: false, uploadedChunks });
});

2. 分片上传接口

app.post('/api/upload', (req, res) => {
  const { chunk, hash, index } = req.files;
  const chunkDir = path.resolve(__dirname, 'uploads', hash);

  if (!fs.existsSync(chunkDir)) {
    fs.mkdirSync(chunkDir);
  }

  // 保存分片
  fs.renameSync(chunk.path, path.resolve(chunkDir, index));
  res.json({ success: true });
});

3. 合并分片接口

app.post('/api/merge', async (req, res) => {
  const { fileHash, fileName } = req.body;
  const chunkDir = path.resolve(__dirname, 'uploads', fileHash);
  const chunks = fs.readdirSync(chunkDir);

  // 按分片索引排序
  chunks.sort((a, b) => a - b);
  
  // 合并文件
  const writeStream = fs.createWriteStream(
    path.resolve(__dirname, 'uploads', `${fileHash}.merge`)
  );

  for (const chunk of chunks) {
    const chunkPath = path.resolve(chunkDir, chunk);
    const buffer = fs.readFileSync(chunkPath);
    writeStream.write(buffer);
    fs.unlinkSync(chunkPath); // 删除分片
  }

  writeStream.end();
  res.json({ success: true });
});

五、优化点

  1. Web Worker计算哈希:避免阻塞主线程。
  2. 并发上传控制:限制同时上传的分片数,减轻浏览器压力。
  3. 错误重试机制:对失败分片自动重试。
  4. 进度条优化:使用更友好的UI组件展示进度。