Vue中文社区

大文件分片上传:化整为零!

在现代Web应用中,用户上传文件已成为一项基本功能。然而,当遇到视频、大型设计图纸、数据集等动辄几个GB甚至TB级别的大文件时,传统的直接上传方式就显得力不从心了。它面临着网络不稳定、服务器限制、传输超时等诸多挑战。此时,大文件分片上传 技术便成为了解决这些问题的关键方案。

一、 什么是分片上传?

分片上传,顾名思义,就是将一个大文件按照固定或动态的大小,切割成多个小的数据块(即“分片”)。然后,前端将这些分片逐个或并发地上传到服务器。服务器接收到所有分片后,再将它们按照正确的顺序合并,还原成完整的原始文件。

这个过程可以形象地理解为:运送一本厚厚的书,我们不直接搬动整本书,而是将其拆分成一页一页(或几页一页),分批邮寄,最后在目的地重新装订成册。

二、 为什么需要分片上传?优势何在?

与传统单次上传相比,分片上传带来了以下显著优势:

  1. 突破文件大小限制

  • 前端限制: 浏览器、Web服务器(如Nginx)通常对单次请求的Body大小有限制。分片上传使每个请求的体积都变小,从而绕开了这一限制。
  • 后端限制: 后端语言(如PHP、Java)及其应用服务器也可能有默认的文件上传大小限制。
  • 增强传输稳定性与容错能力

    • 断点续传: 这是分片上传最核心的优势之一。如果网络中断或上传失败,只需要重新上传失败的那个或那几个分片即可,无需从零开始,极大地节省了时间和带宽。
    • 失败重试: 单个分片上传失败,不会影响其他已成功上传的分片,重试的成本极低。
  • 提升上传速度

    • 并发上传: 前端可以同时发起多个HTTP请求,分别上传不同的分片,充分利用用户的网络带宽,从而在整体上缩短上传时间。
  • 便于做进度管理

    • 前端可以精确地追踪到已经成功上传了多少个分片,从而实时计算并展示一个准确的上传进度条,用户体验更好。
  • 服务端负载均衡

    • 分片可以分发到不同的服务器上进行存储和处理,为后续的分布式文件存储和计算奠定了基础。

    三、 分片上传的技术实现流程

    下面清晰地展示了分片上传的核心工作流程:

    flowchart TD
        A[开始上传大文件] --> B[前端 文件分片]
        B --> C[为每个分片生成哈希值<br>用于服务端去重]
        C --> D[上传第一个分片<br>并携带文件信息<br>服务端创建上传任务]
        D --> E[并发上传剩余分片]
        E --> F{所有分片上传成功?}
        F -- 是 --> G[通知服务端合并所有分片]
        F -- 否 --> E
        G --> H[服务端按序合并分片]
        H --> I[删除临时分片文件<br>保存合并后的文件]
        I --> J[上传成功]

    下面我们来详细解读图中的每一个关键步骤:

    前端关键步骤:

    1. 文件选择与读取: 用户通过 <input type="file"> 选择文件后,前端可以使用 File API 和 FileReader 或 Blob.prototype.slice() 方法来读取文件并切割。
    2. 计算文件哈希(可选但推荐): 在分片前,可以计算整个文件的哈希值(如MD5、SHA-1)。这个哈希值可以作为文件的唯一标识,用于服务端的秒传判断(如果已有相同文件,则直接返回URL,无需再传)和分片去重。
    3. 文件分片: 使用 Blob.prototype.slice 方法,像操作数组一样切割文件对象。
      // 假设 file 是用户选择的 File 对象
      const chunkSize = 5 * 1024 * 1024; // 每个分片5MB
      const totalChunks = Math.ceil(file.size / chunkSize);
      const chunks = [];

      for (let i = 0; i < totalChunks; i++) {
      const start = i * chunkSize;
      const end = Math.min(file.size, start + chunkSize);
      const chunk = file.slice(start, end);
        chunks.push(chunk);
      }

    4. 上传分片:
    • uploadId: 本次上传任务的ID。
    • chunkIndex: 当前分片的索引(从0开始)。
    • totalChunks: 总分片数。
    • fileHash: 文件哈希(用于标识文件和解决定位分片存储位置)。
    • 初始化上传: 首先,前端可以发送一个请求告知服务端即将开始一个文件的上传,并携带文件信息(文件名、大小、哈希、总分片数)。服务端可以返回一个本次上传的唯一 uploadId,并检查是否已存在该文件。
    • 并发上传: 前端使用 Promise.all 或类似机制并发地上传所有分片。每个分片的上传请求应携带关键信息:
    • 进度追踪: 监听每个分片的上传进度,汇总后更新全局进度条。

    服务端关键步骤:

    1. 接收初始化请求: 验证文件信息,创建上传任务,生成 uploadId。检查文件哈希,如果已存在,则直接返回“秒传”成功。
    2. 接收并存储分片:
    • 根据 uploadId 和 chunkIndex 唯一标识一个分片。
    • 将分片文件以临时形式存储。常见的做法是在特定临时目录下,创建以 uploadId 或 fileHash 命名的文件夹,然后将分片命名为 chunkIndex 的形式(例如 0.chunk, 1.chunk)。
  • 检查分片上传状态: 前端在上传每个分片前,可以先询问服务端该分片是否已存在,以实现更精细的断点续传。
  • 合并分片:
    • 当前端通知服务端所有分片已上传完毕时,服务端开始合并。
    • 按序合并至关重要! 根据 chunkIndex 的顺序,将所有分片文件的内容读取并追加到最终文件中。
    • 合并完成后,将文件移动到永久存储位置,并删除临时分片文件夹。

    四、 核心代码示例(概念版)

    前端(JavaScript):

    asyncfunctionuploadFile(file) {
    const chunkSize = 5 * 1024 * 1024;
    const totalChunks = Math.ceil(file.size / chunkSize);
    const fileHash = await calculateFileHash(file); // 计算文件哈希的函数

    // 1. 初始化上传,获取 uploadId
    const { uploadId } = await api.initUpload({
    fileName: file.name,
    fileSize: file.size,
        fileHash,
        totalChunks
      });

    // 2. 创建分片并上传
    const uploadPromises = [];
    for (let i = 0; i < totalChunks; i++) {
    const start = i * chunkSize;
    const end = Math.min(file.size, start + chunkSize);
    const chunk = file.slice(start, end);

    // 封装一个上传单个分片的函数
    const uploadChunkPromise = api.uploadChunk(uploadId, chunk, i, fileHash);
        uploadPromises.push(uploadChunkPromise);
      }

    // 3. 等待所有分片上传完成
    awaitPromise.all(uploadPromises);

    // 4. 通知服务端合并文件
    await api.mergeChunks(uploadId, fileHash);
    }

    // API 方法示例
    const api = {
    async initUpload(data) {
    const resp = await fetch('/api/upload/init', { method: 'POST', body: JSON.stringify(data) });
    returnawait resp.json();
      },
    async uploadChunk(uploadId, chunk, chunkIndex, fileHash) {
    const formData = new FormData();
        formData.append('uploadId', uploadId);
        formData.append('chunk', chunk);
        formData.append('chunkIndex', chunkIndex);
        formData.append('fileHash', fileHash);

    await fetch('/api/upload/chunk', { method: 'POST', body: formData });
      },
    async mergeChunks(uploadId, fileHash) {
    await fetch('/api/upload/merge', {
    method: 'POST',
    body: JSON.stringify({ uploadId, fileHash })
        });
      }
    };

    服务端(Node.js + Express 概念示例):

    // 初始化上传
    app.post('/api/upload/init', (req, res) => {
    const { fileHash } = req.body;
    // 检查文件是否已存在(秒传)
    if (fs.existsSync(`./uploads/${fileHash}.complete`)) {
    return res.json({ code: 200, message: 'File already exists', uploaded: true });
      }
    // 生成 uploadId
    const uploadId = generateUniqueId();
    // 创建临时目录
      fs.mkdirSync(`./temp/${uploadId}`, { recursive: true });
      res.json({ uploadId, uploaded: false });
    });

    // 上传分片
    app.post('/api/upload/chunk', upload.single('chunk'), (req, res) => {
    const { uploadId, chunkIndex, fileHash } = req.body;
    const chunk = req.file;
    // 将分片保存到临时目录
    const chunkPath = `./temp/${uploadId}/${chunkIndex}`;
      fs.renameSync(chunk.path, chunkPath);
      res.json({ code: 200, message: 'Chunk uploaded' });
    });

    // 合并分片
    app.post('/api/upload/merge', async (req, res) => {
    const { uploadId, fileHash } = req.body;
    const tempDir = `./temp/${uploadId}`;
    const chunks = fs.readdirSync(tempDir).sort((a, b) => a - b);
    const finalPath = `./uploads/${fileHash}.dat`;

    const writeStream = fs.createWriteStream(finalPath);
    for (const chunk of chunks) {
    const chunkPath = `${tempDir}/${chunk}`;
    const buffer = fs.readFileSync(chunkPath);
        writeStream.write(buffer);
        fs.unlinkSync(chunkPath); // 删除分片
      }
      writeStream.end();
      fs.rmdirSync(tempDir); // 删除临时目录

    // 创建一个标记文件,表示该文件已上传完成,用于秒传
      fs.writeFileSync(`./uploads/${fileHash}.complete`, '');
      res.json({ code: 200, message: 'File merged successfully' });
    });

    五、 总结

    大文件分片上传是一项通过“分而治之”思想解决复杂问题的经典实践。它不仅解决了技术上的限制,更极大地提升了用户体验和系统的鲁棒性。虽然实现起来比简单上传要复杂,但其带来的稳定性、效率和可扩展性优势,使其在处理大文件传输场景中成为了不可或缺的标准方案。在开发类似功能时,务必注意分片的顺序、临时文件的管理和清理,以及完善的错误处理机制。