likes
comments
collection
share

前端大文件秒传能力透析

作者站长头像
站长
· 阅读数 11

作为一名前端从业者,我们经常会遇到大文件上传的场景,而且对于一些已上传过的文件,用户重新上传同样的文件可能会浪费时间和带宽资源。因此,实现大文件秒传已经成为了许多前端开发人员需要解决的问题。本文将介绍如何在前端实现大文件秒传,包括如何利用文件的哈希值、分片上传和服务器端缓存等技术手段,以达到快速上传大文件的目的。

1. 前置知识

大文件上传是Web应用系统中常见的问题, 尽管HTTP是TCP上层的协议, 但是HTTP协议本身并不适合处理超大的请求体, 文件上传的稳定性存在着很大的问题如果传输过程中因某种异常而中断, 将前功尽弃, 同时, 若没有断点续传功能, 那文件只能重新上传, 这样不仅造成带宽资源的浪费, 而且不能保证再次上传文件就能成功。

在深入探讨如何实现大文件秒传之前,我们需要先了解以下几个概念:

1.1 文件哈希值

文件哈希值是文件内容的唯一标识符,通过对文件内容进行哈希算法计算得到的结果。文件内容一旦发生改变,其哈希值也会改变。比较两个文件的哈希值可以确定它们是否相同。

1.2 分片上传

分片上传是指将大文件分割为若干个小块,分别上传到服务器。这种方式可以避免由于网络不稳定导致的上传中断和重传,同时能够更好的利用服务器资源。

1.3 服务器端缓存

服务器端缓存是指在服务器端缓存已上传过的文件,当有用户上传相同的文件时,直接返回服务器端缓存的文件,而不需要对文件进行重新上传。

2. 实现步骤

如何利用文件的哈希值、分片上传和服务器端缓存等技术手段实现大文件秒传呢?下面将详细介绍实现步骤。

2.1 计算文件的哈希值

首先,在前端上传文件之前,需要计算文件的哈希值。常见的哈希算法有 MD5 和 SHA-1 等。在实际应用中,一般使用的是 MD5 算法,因为其计算速度较快,且被广泛应用于文件校验等领域。

function calculateHash(file) {
  return new Promise((resolve, reject) => {
    const chunkSize = 2 * 1024 * 1024; // 每次读取 2MB
    let chunks = Math.ceil(file.size / chunkSize); // 计算分片数
    let currentChunk = 0;
    let spark = new SparkMD5.ArrayBuffer();
    let fileReader = new FileReader();

    // 递归处理每个分片
    function loadNext() {
      let start = currentChunk * chunkSize;
      let end =
        start + chunkSize > file.size ? file.size : start + chunkSize;

      fileReader.readAsArrayBuffer(file.slice(start, end));
      fileReader.onload = (event) => {
        spark.append(event.target.result); // 将分片添加到哈希对象
        currentChunk++;

        if (currentChunk < chunks) {
          loadNext(); // 继续处理下一个分片
        } else {
          let hash = spark.end(); // 计算哈希值
          resolve(hash); // 返回哈希值
        }
      };
      fileReader.onerror = reject;
    }

    loadNext();
  });
}

在上述代码中,我们使用了 SparkMD5 库来计算文件的哈希值。该库基于 JavaScript 实现了 MD5 算法,并且支持将大文件分片处理,以避免因为一次性读取整个文件而导致的内存溢出等问题。

2.2 判断文件是否已上传

接下来,在上传文件之前,需要向服务器发送请求,判断该文件是否已经上传过。如果已经上传过,可以直接返回服务器端缓存的文件,否则需要进行分片上传。

async function uploadFile(file) {
  let hash = await calculateHash(file);
  let result = await checkFile(hash);

  if (result.code === 0) {
    // 文件已上传过,直接返回文件地址
    return result.data.url;
  } else {
    // 文件未上传过,进行分片上传
    await uploadChunks(file, hash, result.data.chunkSize);
    let res = await mergeChunks(file, result.data.fileName);
    return res.url;
  }
}

function checkFile(hash) {
  return axios.get("/api/check", { params: { hash } }).then((res) => res.data);
}

在上述代码中,我们使用 axios 库发送 GET 请求,获取服务器返回的关于该文件的信息。如果该文件已上传过,返回的数据中会包含文件的地址,否则返回的数据中会包含文件的分片大小和服务器端缓存的文件名。

2.3 分片上传

如果该文件未上传过,需要进行分片上传。首先,将文件按照设定的分片大小进行分割,并计算每个分片的哈希值。然后,依次上传每个分片,并在每个分片上传成功后发送请求通知服务器该分片已上传成功。

async function uploadChunks(file, hash, chunkSize) {
  const chunks = Math.ceil(file.size / chunkSize);
  let chunkList = [];

  // 递归处理每个分片
  async function processChunk(start, index) {
    return new Promise((resolve, reject) => {
      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", hash);
      formData.append("filename", file.name);
      formData.append("index", index);

      axios.post("/api/upload", formData, {
        headers: { "Content-Type": "multipart/form-data" },
      })
        .then((res) => {
          if (res.data.code === 0) {
            chunkList[index] = res.data.data;
            resolve(res.data.data);
          } else {
            reject(res.data.msg);
          }
        })
        .catch(reject);
    });
  }

  for (let i = 0; i < chunks; i++) {
    await processChunk(i * chunkSize, i);
  }

  return chunkList;
}

在上述代码中,我们使用 axios 库发送 POST 请求,将每个分片上传到服务器。请求数据中包含文件的哈希值、名称和分片编号等信息。上传完成后,服务器会返回当前分片的信息,包括文件名、分片编号和文件路径等信息。

2.4 合并分片

当分片全部上传成功后,需要将这些分片合并成原文件。在服务器端,可以通过读取每个分片的内容,并根据分片编号将它们拼接起来形成原文件。然而,在前端中,我们也可以通过 Blob 对象和 URL.createObjectURL 方法将分片拼接成原文件。

async function mergeChunks(file, fileName) {
  // 拼接分片
  const chunkList = await checkFile(fileName);
  let blobList = chunkList.map((chunk) => {
    return axios.get(chunk.path, {
      responseType: "blob",
    }).then((res) => res.data);
  });
  let blob = new Blob(await Promise.all(blobList), { type: file.type });

  // 上传原文件
  const formData = new FormData();
  formData.append("file", blob);
  formData.append("filename", file.name);

  return axios.post("/api/merge", formData, {
    headers: { "Content-Type": "multipart/form-data" },
  }).then((res) => res.data);
}

在上述代码中,我们首先通过 checkFile 函数获取分片列表,并依次读取每个分片的内容,然后使用 Promise.all 方法等待所有分片读取完成,并通过 Blob 构造函数将分片拼接成原文件。最后,使用 axios 库将原文件上传到服务器。

3. 总结

本文介绍了如何利用文件的哈希值、分片上传和服务器端缓存等技术手段实现前端大文件秒传。在实现过程中,我们需要计算文件的哈希值、判断文件是否已上传、进行分片上传和合并分片等步骤。同时,为了保证代码的可读性和可维护性,我们需要采用模块化编程的方式,并且注重错误处理和异常捕获。