js大文件的并发上传

本文将介绍如何利用 async-pool 这个库提供的 asyncPool 函数来实现大文件的并发分片上传。相信有些小伙伴已经了解大文件上传的解决方案,在上传大文件时,为了提高上传的效率,我们一般会使用 Blob.slice 方法对大文件按照指定的大小进行切割,然后通过多线程进行分块上传,等所有分块都成功上传后,再通知服务端进行分块合并。

在大文件上传的场景中,我们将使用 Blob.slice 方法对大文件按照指定的大小进行切割,然后对分块进行并行上传。接下来,我们来看一下具体如何实现大文件上传。

在这里插入图片描述

看完上图相信你对大文件上传的方案,已经有了一定的了解。

1.如何实现大文件上传?

为了让大家能够更好地理解后面的内容,我们先来看一下整体的流程图:

在这里插入图片描述

了解完大文件上传的流程之后,我们先来定义上述流程中涉及的一些辅助函数。

1.1 定义辅助函数

1.1.1 定义 calcFileMD5 函数

顾名思义 calcFileMD5 函数,用于计算文件的 MD5 值(数字指纹)。在该函数中,我们使用 FileReader API 分块读取文件的内容,然后通过 spark-md5 这个库提供的方法来计算文件的 MD5 值。

  function calcFileMD5(file) {
    return new Promise((resolve, reject) => {
      let chunkSize = 2097152, // 2M
        chunks = Math.ceil(file.size / chunkSize),
        currentChunk = 0,
        spark = new SparkMD5.ArrayBuffer(),
        fileReader = new FileReader();

      fileReader.onload = (e) => {
        spark.append(e.target.result);
        currentChunk++;
        if (currentChunk < chunks) {
          loadNext();
        } else {
          resolve(spark.end());
        }
      };

      fileReader.onerror = (e) => {
        reject(fileReader.error);
        reader.abort();
      };

      function loadNext() {
        let start = currentChunk * chunkSize,
          end = start + chunkSize >= file.size ? file.size : start + chunkSize;
        fileReader.readAsArrayBuffer(file.slice(start, end));
      }
      loadNext();
    });
  }

1.1.2 定义 asyncPool 函数

它用于实现异步任务的并发控制。该函数接收 3 个参数:

poolLimit(数字类型):表示限制的并发数;
array(数组类型):表示任务数组;
iteratorFn(函数类型):表示迭代函数,用于实现对每个任务项进行处理,该函数会返回一个 Promise 对象或异步函数。

  async function asyncPool(poolLimit, array, iteratorFn) {
    const ret = []; // 存储所有的异步任务
    const executing = []; // 存储正在执行的异步任务
    for (const item of array) {
      // 调用iteratorFn函数创建异步任务
      const p = Promise.resolve().then(() => iteratorFn(item, array));
      ret.push(p); // 保存新的异步任务

      // 当poolLimit值小于或等于总任务个数时,进行并发控制
      if (poolLimit <= array.length) {
        // 当任务完成后,从正在执行的任务数组中移除已完成的任务
        const e = p.then(() => executing.splice(executing.indexOf(e), 1));
        executing.push(e); // 保存正在执行的异步任务
        if (executing.length >= poolLimit) {
          await Promise.race(executing); // 等待较快的任务执行完成
        }
      }
    }
    return Promise.all(ret);
  }

1.1.3 定义 upload 函数

在 upload 函数内,我们使用了前面介绍的 asyncPool 函数来实现异步任务的并发控制,具体如下所示:

  function upload({url, file, fileMd5, fileSize, chunkSize, chunkIds, poolLimit = 1}) {
    const chunks = typeof chunkSize === "number" ? Math.ceil(fileSize / chunkSize) : 1;
    return asyncPool(poolLimit, [...new Array(chunks).keys()], (i) => {
      if (chunkIds.indexOf(i + "") !== -1) { // 已上传的分块直接跳过
        return Promise.resolve();
      }
      let start = i * chunkSize;
      let end = i + 1 == chunks ? fileSize : (i + 1) * chunkSize;
      const chunk = file.slice(start, end); // 对文件进行切割
      return uploadChunk({
        url,
        chunk,
        chunkIndex: i,
        fileMd5,
        fileName: file.name,
      });
    });
  }

对于切割完的文件块,会通过 uploadChunk 函数,来执行实际的上传操作:

  function uploadChunk({ url, chunk, chunkIndex, fileMd5, fileName }) {
    let formData = new FormData();
    formData.set("file1", chunk, fileMd5 + "-" + chunkIndex);
    formData.set("name", fileName);
    formData.set("timestamp", Date.now());
    return request.post(url, formData);
  }

1.1.4 定义 concatFiles 函数

当所有分块都上传完成之后,我们需要通知服务端执行分块合并操作,这里我们定义了 concatFiles 函数来实现该功能:

  function concatFiles(url, name, md5) {
    return request.get(url, { params: { name, md5 } });
  }

1.1.5 定义 uploadFile 函数

 async function uploadFile() {
	// files文件上传对象
    if (!files) return;
    const fileMd5 = await calcFileMD5(files); // 计算文件的MD5
    await upload({
        url: "/upload",
        file: files, // 文件对象
        fileMd5, // 文件MD5值
        fileSize: files.size, // 文件大小
        chunkSize: 0.1 * 1024 * 1024, // 分块大小
        chunkIds: [], // 已上传的分块列表
        poolLimit: 3, // 限制的并发数
      });
    console.log('完成');
    const resp = await concatFiles("/concatFiles", files.name, fileMd5);
    console.log(resp);
  }

1.2 大文件并发上传示例


<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>Title</title>
  <script src="https://cdn.bootcdn.net/ajax/libs/spark-md5/3.0.0/spark-md5.min.js"></script>
</head>
<body>
<input type="file" id="uploadFile" onchange="handleChange(event)"/>
<button id="submit" onclick="uploadFile()">上传文件</button>
</body>
<script>
  let files = null;
  function handleChange(e) {
    try{
      files = e.target.files[0];
      e.target.value = '';
    }catch (e) {
      files = null;
    }
  }

  function calcFileMD5(file) {
    return new Promise((resolve, reject) => {
      let chunkSize = 2097152, // 2M
        chunks = Math.ceil(file.size / chunkSize),
        currentChunk = 0,
        spark = new SparkMD5.ArrayBuffer(),
        fileReader = new FileReader();

      fileReader.onload = (e) => {
        spark.append(e.target.result);
        currentChunk++;
        if (currentChunk < chunks) {
          loadNext();
        } else {
          resolve(spark.end());
        }
      };

      fileReader.onerror = (e) => {
        reject(fileReader.error);
        reader.abort();
      };

      function loadNext() {
        let start = currentChunk * chunkSize,
          end = start + chunkSize >= file.size ? file.size : start + chunkSize;
        fileReader.readAsArrayBuffer(file.slice(start, end));
      }
      loadNext();
    });
  }

  async function checkFileExist(url, fileName, fileMd5) {
    return new Promise(((resolve, reject) => {
      setTimeout(() => {
        randomNum = Math.random()
        const obj = {
          data: {
            isExists: randomNum > 5,
            url: randomNum > 5 ? 'https://www.baidu.com/' : null,
            chunkIds: []
          }
        }
        resolve(obj)
      }, 500)
    }))
  }

  function uploadChunk({ url, chunk, chunkIndex, fileMd5, fileName }) {
    let formData = new FormData();
    formData.set("file1", chunk, fileMd5 + "-" + chunkIndex);
    formData.set("name", fileName);
    formData.set("timestamp", Date.now());
    const data = {
      file: chunk,
      fileMd5: fileMd5 + "-" + chunkIndex,
      name: fileName,
      timestamp: Date.now()
    }
    console.log(chunkIndex, data);
    // 调用后端接口,这里模拟了后端接口
    return new Promise(((resolve, reject) => {
      setTimeout(() => {
        resolve(data)
      }, ~~(Math.random() * (3000 - 1000 + 1) + 1000))
    }))
  }

  async function asyncPool(poolLimit, array, iteratorFn) {
    const ret = []; // 存储所有的异步任务
    const executing = []; // 存储正在执行的异步任务
    for (const item of array) {
      // 调用iteratorFn函数创建异步任务
      const p = Promise.resolve().then(() => iteratorFn(item, array));
      ret.push(p); // 保存新的异步任务

      // 当poolLimit值小于或等于总任务个数时,进行并发控制
      if (poolLimit <= array.length) {
        // 当任务完成后,从正在执行的任务数组中移除已完成的任务
        const e = p.then(() => executing.splice(executing.indexOf(e), 1));
        executing.push(e); // 保存正在执行的异步任务
        if (executing.length >= poolLimit) {
          await Promise.race(executing); // 等待较快的任务执行完成
        }
      }
    }
    return Promise.all(ret);
  }

  function upload({url, file, fileMd5, fileSize, chunkSize, chunkIds, poolLimit = 1}) {
    const chunks = typeof chunkSize === "number" ? Math.ceil(fileSize / chunkSize) : 1;
    return asyncPool(poolLimit, [...new Array(chunks).keys()], (i) => {
      if (chunkIds.indexOf(i + "") !== -1) { // 已上传的分块直接跳过
        return Promise.resolve();
      }
      let start = i * chunkSize;
      let end = i + 1 == chunks ? fileSize : (i + 1) * chunkSize;
      const chunk = file.slice(start, end); // 对文件进行切割
      return uploadChunk({
        url,
        chunk,
        chunkIndex: i,
        fileMd5,
        fileName: file.name,
      });
    });
  }

  function concatFiles(url, name, md5) {
   // 调用后端接口,这里模拟了后端接口
    return new Promise((resolve, reject) => {
      randomNum = Math.random()
      const data = {
        status: randomNum > 0.5 ? 1 : 0,
        url: randomNum > 0.5 ? 'https://www.baidu.com/' : null
      }
      resolve(data)
    })
  }

  async function uploadFile() {
    if (!files) return;
    const fileMd5 = await calcFileMD5(files); // 计算文件的MD5, 这里也可以自己实现一个唯一值,也可后端生成主键ID
       await upload({
        url: "/single",
        file: files, // 文件对象
        fileMd5, // 文件MD5值
        fileSize: files.size, // 文件大小
        chunkSize: 0.1 * 1024 * 1024, // 分块大小
        chunkIds: [], // 已上传的分块列表
        poolLimit: 3, // 限制的并发数
      });
    console.log('完成');
    const resp = await concatFiles("/concatFiles", files.name, fileMd5);
    console.log(resp);
  }

</script>
</html>

由于完整的示例代码内容比较多,这里就不放具体的代码了。

2、总结

本文介绍了在 JavaScript 中如何利用 async-pool 这个库提供的 asyncPool 函数,来实现大文件的并发上传。此外,文中我们也使用了 spark-md5 这个库来计算文件的数字指纹。

如果有遇到不清楚的地方,欢迎随时交流哟。

  • 0
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
Dropzone.js 上传文件后端接收方式与普通文件上传略有不同。由于 Dropzone.js 使用了分片上传,因此后端需要对上传的每个分片进行处理,并在所有分片上传完成后将它们合并成一个完整的文件。以下是一个 PHP 后端接收 Dropzone.js文件上传的示例代码: ```php <?php $targetDir = "uploads/"; // 上传文件保存目录 $targetFile = $targetDir . $_POST["dzuuid"] . "_" . $_POST["dzchunkindex"]; // 分片文件路径 $chunkSize = intval($_POST["dzchunksize"]); // 分片大小 $totalSize = intval($_POST["dztotalfilesize"]); // 文件总大小 $chunkIndex = intval($_POST["dzchunkindex"]); // 当前分片索引 // 创建上传目录 if (!file_exists($targetDir)) { mkdir($targetDir, 0777, true); } // 保存上传的分片文件 if (move_uploaded_file($_FILES["file"]["tmp_name"], $targetFile)) { // 检查所有分片是否已上传完成 $totalChunks = ceil($totalSize / $chunkSize); for ($i = 0; $i < $totalChunks; $i++) { $chunkFile = $targetDir . $_POST["dzuuid"] . "_" . $i; if (!file_exists($chunkFile)) { die("分片未上传完成"); } } // 将所有分片合并成一个完整的文件 $finalFile = $targetDir . $_POST["dzuuid"]; $fp = fopen($finalFile, "wb"); for ($i = 0; $i < $totalChunks; $i++) { $chunkFile = $targetDir . $_POST["dzuuid"] . "_" . $i; $fp2 = fopen($chunkFile, "rb"); stream_copy_to_stream($fp2, $fp); fclose($fp2); unlink($chunkFile); // 删除已合并的分片文件 } fclose($fp); // 返回上传结果 echo json_encode(array( "success" => true, "filename" => $_POST["dzuuid"] )); } else { echo json_encode(array( "success" => false, "message" => "上传失败" )); } ?> ``` 上述代码中,我们首先获取上传的分片文件信息,包括分片大小、文件总大小、当前分片索引等。接着,将上传的分片文件保存到指定目录,并检查所有分片是否已上传完成。如果所有分片都已上传完成,则将它们合并成一个完整的文件。最后,返回上传结果,包括上传是否成功和上传后的文件名。 需要注意的是,上述代码仅为示例,具体实现需要根据实际情况进行调整。例如,可以根据上传文件的类型、大小、格式等进行校验和过滤,避免恶意文件上传文件格式不兼容等问题。另外,需要注意文件上传过程中的并发数、上传速度等问题,以保证上传效率和稳定性。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值