断点续传,一秒也不能少:从前端到后端实现大文件上传的完美体验

224 阅读3分钟

当需要上传大文件时,传统的上传方式可能会导致上传失败或上传时间过长,影响用户体验。为了提高上传效率和稳定性,大文件上传常常采用分片上传的方式,并结合断点续传实现更好的用户体验。本文将介绍如何使用前端技术和服务端技术实现大文件上传的断点续传功能。

  1. 前端处理

前端处理主要包括文件切片和上传。

1.1 文件切片 将大文件切成多个小文件,每个小文件大小为固定值(如 2MB),并记录每个小文件的信息,如大小、文件名、总块数、当前块数等信息。文件切片可以使用 File.slice() 方法实现,该方法接受两个参数,分别是起始位置和结束位置,可以将文件切成指定大小的块。

function sliceFile(file, chunkSize) {
  const chunks = [];
  let start = 0;
  let end = chunkSize;
  let index = 0;

  while (start < file.size) {
    chunks.push({
      index,
      data: file.slice(start, end),
    });
    start = end;
    end = start + chunkSize;
    index++;
  }

  return chunks;
}

1.2 上传 使用 XMLHttpRequest 或 Fetch 发送切片数据到服务端,服务端收到数据后将数据存储在指定路径下。上传过程中需要实现断点续传,即在上传过程中如果网络出现异常或者用户暂停上传,下次上传可以从上一次上传的位置继续上传,避免重复上传已经上传的数据。

function upload(file, chunkSize, url, options = {}) {
  const chunks = sliceFile(file, chunkSize);
  const totalChunks = chunks.length;
  let uploadedChunks = 0;

  const defaultOptions = {
    headers: {},
    onProgress: () => {},
    onStart: () => {},
    onSuccess: () => {},
    onError: () => {},
  };

  const { headers, onProgress, onStart, onSuccess, onError } = {
    ...defaultOptions,
    ...options,
  };

  const uploadChunk = async (chunk) => {
    const formData = new FormData();
    formData.append('file', chunk.data);
    formData.append('index', chunk.index);
    formData.append('name', file.name);
    formData.append('size', file.size);
    formData.append('totalChunks', totalChunks);

    const xhr = new XMLHttpRequest();

    xhr.upload.onprogress = (event) => {
      onProgress(
        (uploadedChunks * chunkSize + event.loaded) / file.size,
        event
      );
    };

    xhr.upload.onerror = (event) => {
      onError(event);
    };

    xhr.upload.onabort = (event) => {
      onError(event);
    };

    xhr.upload.onload = async () => {
      uploadedChunks++;

      if (uploadedChunks === totalChunks) {
        onSuccess();
        return;
      }

      await uploadChunk(chunks[uploadedChunks]);
    };

    xhr.open('POST', url);
    Object.entries(headers).forEach(([key, value]) => {
      xhr.setRequestHeader(key, value);
    });

    xhr.send(formData);
  };

在服务端,需要接收前端上传的文件分片,并将其存储在服务器上。同时,还需要记录已经上传了哪些分片,以及哪些分片还没有被上传。在实现断点续传时,需要注意以下几点:

文件分片的存储:需要将文件分片存储在一个临时目录中,待所有分片上传完成后再将其合并成完整文件并存储在指定的目录中。 记录已经上传的分片:为了实现断点续传,需要记录已经上传了哪些分片。可以将上传的分片信息存储在一个数据库中,也可以将其存储在一个文件中。 合并分片:在所有分片都上传完成后,需要将这些分片合并成完整的文件,并存储在指定的目录中。 以下是一个使用 Node.js 的示例代码,可以帮助你实现大文件上传的断点续传功能。

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

// 临时存储文件的目录
const tempDir = path.resolve(__dirname, 'temp');

// 上传文件的接口
app.post('/upload', (req, res) => {
  const { fileName, total, index } = req.body; // 文件名,总片数,当前片数
  const fileDir = path.resolve(tempDir, fileName); // 文件夹目录
  const filePath = path.resolve(fileDir, index.toString()); // 当前分片存储路径

  // 创建文件夹
  if (!fs.existsSync(fileDir)) {
    fs.mkdirSync(fileDir, { recursive: true });
  }

  // 将分片写入到文件中
  req.pipe(fs.createWriteStream(filePath));

  // 如果所有分片都上传完成,则合并分片
  if (+index === +total - 1) {
    const writeStream = fs.createWriteStream(
      path.resolve(__dirname, 'uploads', fileName)
    );
    for (let i = 0; i < total; i++) {
      const chunkPath = path.resolve(fileDir, i.toString());
      fs.appendFileSync(writeStream, fs.readFileSync(chunkPath));
      fs.unlinkSync(chunkPath); // 删除已经合并的分片
    }
    res.send({ message: '上传成功' });
  } else {
    res.send({ message: '分片上传成功' });
  }
});
app.listen(3000, () => console.log('服务器启动成功'));

在上述代码中,我们使用了 Express 库来创建一个简单的 HTTP 服务器。在上传文件的接口中,我们根据请求中携带的信息将分片写入到指定的目录中,并记录上传的分片信息。在所有分片都上传完成后,我们使用 fs 模块将这些分片合并成完整的文件,并存储到指定的目录中。

总结

大文件上传断点续传,虽然看起来是个严肃的技术问题,但是经过我们这篇文章的讲解,相信你已经掌握了它的实现方法。如果你还没掌握,那么也别太沮丧,毕竟这个技术问题跟学习驾照一样,考不过一次就再来一次,直到你掌握为止!另外,小编私下里告诉你们一个小秘密:实现大文件上传断点续传的关键在于心态要放轻松,一定要给自己放个假,放个“假点续传”,这样才能事半功倍!