大文件上传实现

通过分片上传、断点续传等方式实现大文件上传

问题

如何实现大文件上传?

解答

大文件上传的核心思路是将文件变小,通过压缩或分块后再上传。主要有以下几种实现方式:

1. 分片上传

将大文件拆分成小的文件块(chunk),通过多个并行请求依次上传。服务器接收后存储,最后合并所有文件块还原原始文件。这种方法可以降低单个请求负载,支持断点续传。

前端分片实现:

// 使用 File.prototype.slice 方法分割文件
function createChunks(file, chunkSize = 2 * 1024 * 1024) {
  const chunks = [];
  let start = 0;
  
  while (start < file.size) {
    const chunk = file.slice(start, start + chunkSize);
    chunks.push(chunk);
    start += chunkSize;
  }
  
  return chunks;
}

// 上传分片
async function uploadChunks(file, chunks) {
  const fileHash = await calculateHash(file); // 计算文件 hash
  
  const uploadPromises = chunks.map((chunk, index) => {
    const formData = new FormData();
    formData.append('chunk', chunk);
    formData.append('hash', fileHash);
    formData.append('index', index);
    formData.append('total', chunks.length);
    
    return fetch('/upload', {
      method: 'POST',
      body: formData
    });
  });
  
  // 等待所有分片上传完成
  await Promise.all(uploadPromises);
  
  // 通知服务端合并
  await fetch('/merge', {
    method: 'POST',
    body: JSON.stringify({ hash: fileHash, total: chunks.length })
  });
}

服务端合并实现(Node.js):

const fs = require('fs');
const path = require('path');

function mergeChunks(fileHash, total, outputPath) {
  const writeStream = fs.createWriteStream(outputPath);
  
  for (let i = 0; i < total; i++) {
    const chunkPath = path.join(__dirname, 'chunks', `${fileHash}-${i}`);
    const readStream = fs.createReadStream(chunkPath);
    
    readStream.pipe(writeStream, { end: i === total - 1 });
    
    // 删除分片文件
    readStream.on('end', () => {
      fs.unlinkSync(chunkPath);
    });
  }
}

2. 断点续传

记录上传进度,网络中断后从上次位置继续上传:

async function uploadWithResume(file, chunks) {
  const fileHash = await calculateHash(file);
  
  // 查询已上传的分片
  const uploaded = await fetch(`/check?hash=${fileHash}`).then(r => r.json());
  
  const uploadPromises = chunks.map((chunk, index) => {
    // 跳过已上传的分片
    if (uploaded.includes(index)) {
      return Promise.resolve();
    }
    
    return uploadChunk(chunk, fileHash, index);
  });
  
  await Promise.allSettled(uploadPromises); // 使用 allSettled 处理失败情况
}

3. 其他优化方案

  • 流式上传:客户端使用流方式逐步读取文件,通过 POST 请求发送数据流,减少内存占用
  • 压缩上传:客户端先压缩文件再上传,减少上传时间和带宽消耗
  • 并发控制:限制同时上传的分片数量,避免请求过多
  • 使用云存储服务:如 Amazon S3、阿里云 OSS 等,提供了优化的大文件上传方案

关键点

  • 前端使用 File.prototype.slice 方法分割文件,服务端使用流(readStream/writeStream)合并分片
  • 每个分片携带文件 hash、分片序号和总数,服务端按序号合并保证顺序正确
  • 使用 Promise.all 等待所有分片上传完成后,发送合并请求通知服务端
  • 上传失败时,使用 Promise.allSettled 处理,根据返回的失败信息重传对应分片
  • 实现断点续传需要记录上传进度,查询已上传分片后跳过继续上传