大文件上传

0 阅读3分钟

流程

  1. 设置分片大小,将文件分片:
    • 补充: 需要计算文件的总分片数。总片数 = Math.ceil(文件总大小 / 分片大小)
    • 补充: 需要为每个分片生成一个唯一的标识或索引(通常是序号,如 0, 1, 2, ...),以便后端按顺序重组文件。
    • 补充: 需要记录当前文件的一些元信息(如文件名、文件大小、文件类型、唯一标识符(如 MD5 或前端生成的 UUID)、总分片数),这些信息通常会在上传第一个分片或一个单独的初始化请求中发送给后端。
  1. 设置并发处理,上传:
    • 补充: 并发控制: 直接无限制并发可能导致浏览器或服务器压力过大。需要设置一个合理的并发数(如 3-5),使用队列或 Promise.all/Promise.allSettled控制同时上传的分片数量。
    • 补充: 分片上传请求: 每个分片的上传请求需要包含:
      • 分片数据本身(Blob)
      • 分片索引(当前是第几片)
      • 文件唯一标识符(确保后端知道这个分片属于哪个文件)
      • 总分片数(可选,后端也可在初始化时知道)
      • 文件名、文件类型等(可选,通常在初始化请求中已发送)
    • 补充: 上传进度跟踪: 需要监听每个分片上传的进度(XMLHttpRequest.upload.onprogressaxiosonUploadProgress),并汇总计算整个文件的上传进度。
    • 补充: 错误处理与重试: 某个分片上传失败时,需要能够识别并尝试重新上传该分片(通常有重试次数限制)。
    • 补充:完整性校验与合并请求: 当所有分片都上传成功后(包括可能的重试),前端需要发送一个合并请求给后端,告知后端所有分片已上传完毕,可以按分片索引顺序合并成完整的文件。这个请求通常包含文件唯一标识符。
  1. 合并:
    • 确认所有分片(包括重试成功的)都已上传成功。
    • 发送合并请求给后端,包含文件唯一标识符。后端负责将所有属于该标识符的分片按索引顺序合并成完整文件。
  1. 完成/错误处理:
    • 处理合并请求的响应,通知用户上传成功或失败。
    • 处理整个过程中可能出现的网络错误、服务器错误等。
  1. 断点续传
      1. 单次错误处理
      • 接口错误,推送到错误队列。 最后推送错误队列的分片
      1. 刷新页面
        1. 重新上传文件:上传时存储信息到storage,包括有多少片,每片的上传信息(hash、成功与否);上传文件的时候,比对是否是上次的上传文件(通过文件名),如果是,则用信息来上传,否的话重新存储信息到storage。
        1. 不重新上传文件: 上传时把分片的信息,文件内容存储到storage。刷新的时候,判断是否有未完成的,有的话自动上传。(locolstorage只有5mb、 indexDB 50MB-1GB)
// 错误示例:一次性读取整个文件
const readWholeFile = (file) => {
  const reader = new FileReader();
  reader.readAsArrayBuffer(file); // 大文件会导致内存溢出
}

// 正确方案:分片读取
const readInChunks = (file, chunkSize, callback) => {
  let offset = 0;

  const readNextChunk = () => {
    const chunk = file.slice(offset, offset + chunkSize);
    const reader = new FileReader();

    reader.onload = () => {
      callback(reader.result);
      offset += chunkSize;
      if (offset < file.size) {
        readNextChunk();
      }
    };

    reader.readAsArrayBuffer(chunk);
  };

  readNextChunk();
}

3. 并发控制不当

问题:同时上传过多分片导致浏览器卡顿或请求被限制

// 不完善的并发控制
const uploadAllChunks = async (chunks) => {
  // 同时发起所有请求,可能导致问题
  await Promise.all(chunks.map(uploadChunk));
}

// 正确的并发控制
class ConcurrentController {
  constructor(maxConcurrent = 3) {
    this.maxConcurrent = maxConcurrent;
    this.current = 0;
    this.queue = [];
  }

  async add(task) {
    if (this.current >= this.maxConcurrent) {
      await new Promise(resolve => this.queue.push(resolve));
    }

    this.current++;
    try {
      return await task();
    } finally {
      this.current--;
      if (this.queue.length > 0) {
        this.queue.shift()();
      }
    }
  }
}

考虑的优化场景

1. 上传失败了怎么办

2. 怎么做到断点续传

3. a设备上传暂停,b设备想继续上传