JavaScript 中如何实现大文件并发上传?

开发 前端
本文将介绍如何利用 async-pool 这个库提供的 asyncPool 函数来实现大文件的并发上传。

[[402831]]

在 JavaScript 中如何实现并发控制? 这篇文章中,阿宝哥详细分析了 async-pool 这个库如何利用 Promise.all 和 Promise.race 函数实现异步任务的并发控制。之后,阿宝哥通过 JavaScript 中如何实现大文件并行下载? 这篇文章介绍了 async-pool 这个库的实际应用。

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

看完上图相信你对大文件上传的方案,已经有了一定的了解。接下来,我们先来介绍 Blob 和 File 对象。

一、Blob 和 File 对象

1.1 Blob 对象

Blob(Binary Large Object)表示二进制类型的大对象。在数据库管理系统中,将二进制数据存储为一个单一个体的集合。Blob 通常是影像、声音或多媒体文件。在 JavaScript 中 Blob 类型的对象表示不可变的类似文件对象的原始数据。 为了更直观的感受 Blob 对象,我们先来使用 Blob 构造函数,创建一个 myBlob 对象,具体如下图所示:

如你所见,myBlob 对象含有两个属性:size 和 type。其中 size 属性用于表示数据的大小(以字节为单位),type 是 MIME 类型的字符串。Blob 由一个可选的字符串 type(通常是 MIME 类型)和 blobParts 组成:

Blob 表示的不一定是 JavaScript 原生格式的数据。比如 File 接口基于 Blob,继承了 Blob 的功能并将其扩展使其支持用户系统上的文件。

1.2 File 对象

通常情况下, File 对象是来自用户在一个 元素上选择文件后返回的 FileList 对象,也可以是来自由拖放操作生成的 DataTransfer 对象,或者来自 HTMLCanvasElement 上的 mozGetAsFile() API。

File 对象是特殊类型的 Blob,且可以用在任意的 Blob 类型的上下文中。比如说 FileReader、URL.createObjectURL() 及 XMLHttpRequest.send() 都能处理 Blob 和 File。在大文件上传的场景中,我们将使用 Blob.slice 方法对大文件按照指定的大小进行切割,然后对分块进行并行上传。接下来,我们来看一下具体如何实现大文件上传。

二、如何实现大文件上传

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

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

2.1 定义辅助函数

2.1.1 定义 calcFileMD5 函数

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

  1. function calcFileMD5(file) { 
  2.   return new Promise((resolve, reject) => { 
  3.     let chunkSize = 2097152, // 2M 
  4.       chunks = Math.ceil(file.size / chunkSize), 
  5.       currentChunk = 0, 
  6.       spark = new SparkMD5.ArrayBuffer(), 
  7.       fileReader = new FileReader(); 
  8.  
  9.       fileReader.onload = (e) => { 
  10.         spark.append(e.target.result); 
  11.         currentChunk++; 
  12.         if (currentChunk < chunks) { 
  13.           loadNext(); 
  14.         } else { 
  15.           resolve(spark.end()); 
  16.         } 
  17.       }; 
  18.  
  19.       fileReader.onerror = (e) => { 
  20.         reject(fileReader.error); 
  21.         reader.abort(); 
  22.       }; 
  23.  
  24.       function loadNext() { 
  25.         let start = currentChunk * chunkSize, 
  26.           end = start + chunkSize >= file.size ? file.size : start + chunkSize; 
  27.         fileReader.readAsArrayBuffer(file.slice(start, end)); 
  28.       } 
  29.       loadNext(); 
  30.   }); 

2.1.2 定义 asyncPool 函数

在 JavaScript 中如何实现并发控制? 这篇文章中,我们介绍了 asyncPool 函数,它用于实现异步任务的并发控制。该函数接收 3 个参数:

  • poolLimit(数字类型):表示限制的并发数;
  • array(数组类型):表示任务数组;
  • iteratorFn(函数类型):表示迭代函数,用于实现对每个任务项进行处理,该函数会返回一个 Promise 对象或异步函数。
  1. async function asyncPool(poolLimit, array, iteratorFn) { 
  2.   const ret = []; // 存储所有的异步任务 
  3.   const executing = []; // 存储正在执行的异步任务 
  4.   for (const item of array) { 
  5.     // 调用iteratorFn函数创建异步任务 
  6.     const p = Promise.resolve().then(() => iteratorFn(item, array)); 
  7.     ret.push(p); // 保存新的异步任务 
  8.  
  9.     // 当poolLimit值小于或等于总任务个数时,进行并发控制 
  10.     if (poolLimit <= array.length) { 
  11.       // 当任务完成后,从正在执行的任务数组中移除已完成的任务 
  12.       const e = p.then(() => executing.splice(executing.indexOf(e), 1)); 
  13.       executing.push(e); // 保存正在执行的异步任务 
  14.       if (executing.length >= poolLimit) { 
  15.         await Promise.race(executing); // 等待较快的任务执行完成 
  16.       } 
  17.     } 
  18.   } 
  19.   return Promise.all(ret); 

2.1.3 定义 checkFileExist 函数

checkFileExist 函数用于检测文件是否已经上传过了,如果已存在则秒传,否则返回已上传的分块 ID 列表:

  1. function checkFileExist(url, name, md5) { 
  2.   return request.get(url, { 
  3.     params: { 
  4.       name
  5.       md5, 
  6.     }, 
  7.   }).then((response) => response.data); 

在 checkFileExist 函数中使用到的 request 对象是 Axios 实例,通过 axios.create方法来创建:

  1. const request = axios.create({ 
  2.   baseURL: "http://localhost:3000/upload"
  3.   timeout: 10000, 
  4. }); 

有了 request 对象之后,我们就可以轻易地发送 HTTP 请求。在 checkFileExist 函数内部,我们会发起一个 GET 请求,同时携带的查询参数是文件名(name)和文件的 MD5 值。

2.1.4 定义 upload 函数

当调用 checkFileExist 函数之后,如果发现文件尚未上传或者只上传完部分分块的话,就会继续调用 upload 函数来执行上传任务。在 upload 函数内,我们使用了前面介绍的 asyncPool 函数来实现异步任务的并发控制,具体如下所示:

  1. function upload({  
  2.   url, file, fileMd5,  
  3.   fileSize, chunkSize, chunkIds, 
  4.   poolLimit = 1, 
  5. }) { 
  6.   const chunks = typeof chunkSize === "number" ? Math.ceil(fileSize / chunkSize) : 1; 
  7.   return asyncPool(poolLimit, [...new Array(chunks).keys()], (i) => { 
  8.     if (chunkIds.indexOf(i + "") !== -1) { // 已上传的分块直接跳过 
  9.       return Promise.resolve(); 
  10.     } 
  11.     let start = i * chunkSize; 
  12.     let end = i + 1 == chunks ? fileSize : (i + 1) * chunkSize; 
  13.     const chunk = file.slice(start, end); // 对文件进行切割 
  14.     return uploadChunk({ 
  15.       url, 
  16.       chunk, 
  17.       chunkIndex: i, 
  18.       fileMd5, 
  19.       fileName: file.name
  20.     }); 
  21.   }); 

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

  1. function uploadChunk({ url, chunk, chunkIndex, fileMd5, fileName }) { 
  2.   let formData = new FormData(); 
  3.   formData.set("file", chunk, fileMd5 + "-" + chunkIndex); 
  4.   formData.set("name", fileName); 
  5.   formData.set("timestamp"Date.now()); 
  6.   return request.post(url, formData); 

2.1.5 定义 concatFiles 函数

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

  1. function concatFiles(url, name, md5) { 
  2.   return request.get(url, { 
  3.     params: { 
  4.       name
  5.       md5, 
  6.     }, 
  7.   }); 

2.1.6 定义 uploadFile 函数

在前面已定义辅助函数的基础上,我们就可以根据大文件上传的整体流程图来实现一个 uploadFile 函数:

  1. async function uploadFile() { 
  2.   if (!uploadFileEle.files.length) return
  3.   const file = uploadFileEle.files[0]; // 获取待上传的文件 
  4.   const fileMd5 = await calcFileMD5(file); // 计算文件的MD5 
  5.   const fileStatus = await checkFileExist(  // 判断文件是否已存在 
  6.     "/exists",  
  7.     file.name, fileMd5 
  8.   ); 
  9.   if (fileStatus.data && fileStatus.data.isExists) { 
  10.     alert("文件已上传[秒传]"); 
  11.     return
  12.   } else { 
  13.     await upload({ 
  14.       url: "/single"
  15.       file, // 文件对象 
  16.       fileMd5, // 文件MD5值 
  17.       fileSize: file.size, // 文件大小 
  18.       chunkSize: 1 * 1024 * 1024, // 分块大小 
  19.       chunkIds: fileStatus.data.chunkIds, // 已上传的分块列表 
  20.       poolLimit: 3, // 限制的并发数 
  21.      }); 
  22.   } 
  23.   await concatFiles("/concatFiles", file.name, fileMd5); 

2.2 大文件并发上传示例

定义完 uploadFile 函数,要实现大文件并发上传的功能就很简单了,具体代码如下所示:

  1. <!DOCTYPE html> 
  2. <html lang="zh-CN"
  3.   <head> 
  4.     <meta charset="UTF-8" /> 
  5.     <meta name="viewport" content="width=device-width, initial-scale=1.0" /> 
  6.     <meta http-equiv="X-UA-Compatible" content="ie=edge" /> 
  7.     <title>大文件并发上传示例(阿宝哥)</title> 
  8.     <script src="https://cdn.bootcdn.net/ajax/libs/axios/0.21.1/axios.min.js"></script> 
  9.     <script src="https://cdn.bootcdn.net/ajax/libs/spark-md5/3.0.0/spark-md5.min.js"></script> 
  10.   </head> 
  11.   <body> 
  12.     <input type="file" id="uploadFile" /> 
  13.     <button id="submit" onclick="uploadFile()">上传文件</button> 
  14.     <script> 
  15.       const uploadFileEle = document.querySelector("#uploadFile"); 
  16.  
  17.       const request = axios.create({ 
  18.         baseURL: "http://localhost:3000/upload"
  19.         timeout: 10000, 
  20.       }); 
  21.  
  22.       async function uploadFile() { 
  23.         if (!uploadFileEle.files.length) return
  24.      const file = uploadFileEle.files[0]; // 获取待上传的文件 
  25.      const fileMd5 = await calcFileMD5(file); // 计算文件的MD5 
  26.         // ... 
  27.       } 
  28.       // 省略其他函数 
  29.     </script> 
  30.   </body> 
  31. </html> 

由于完整的示例代码内容比较多,阿宝哥就不放具体的代码了。感兴趣的小伙伴,可以访问以下地址浏览客户端和服务器端代码。

  • 完整的示例代码(代码仅供参考,可根据实际情况进行调整):
  • https://gist.github.com/semlinker/b211c0b148ac9be0ac286b387757e692

最后我们来看一下大文件并发上传示例的运行结果:

三、总结

本文介绍了在 JavaScript 中如何利用 async-pool 这个库提供的 asyncPool 函数,来实现大文件的并发上传。此外,文中我们也使用了 spark-md5 这个库来计算文件的数字指纹,如果你数字指纹感兴趣的话,可以阅读 数字指纹有什么用?赶紧来了解一下 这篇文章。

由于篇幅有限,阿宝哥并未介绍服务端的具体代码。其实在做文件分块合并时,阿宝哥是以流的形式进行合并,感兴趣的小伙伴可以自行阅读一下相关代码。如果有遇到不清楚的地方,欢迎随时跟阿宝哥交流哟。

四、参考资源

  • 你不知道的 Blob
  • MDN - File
  • MDN - ArrayBuffer
  • MDN - HTTP请求范围
  • JavaScript 中如何实现并发控制?

 

责任编辑:姜华 来源: 全栈修仙之路
相关推荐

2021-04-19 05:41:04

JavaScript大文件下载

2020-04-02 20:07:17

前端vuenote.js

2022-06-13 14:06:33

大文件上传前端

2021-04-07 06:00:18

JavaScript 前端并发控制

2010-02-05 08:32:32

ASP.NET MVC

2009-12-07 09:45:23

PHP上传大文件设置

2013-03-22 14:42:01

OSS开放存储服务云计算

2022-08-05 08:40:37

架构

2021-01-12 10:22:45

JavaScript并发控制前端

2009-11-16 11:41:19

PHP上传大文件

2009-07-21 15:38:31

2021-01-15 11:40:44

文件Java秒传

2009-07-20 16:09:39

2009-07-08 09:29:58

WebWork

2009-07-21 16:05:58

ASP.NET大文件上

2024-03-27 08:28:31

元素拖拽API文件上传

2010-08-10 16:30:05

Flex上传文件

2010-09-08 16:50:11

JavaScriptDOM操作

2021-05-12 00:03:49

JavaScript

2009-07-14 17:20:31

Webwork文件上传
点赞
收藏

51CTO技术栈公众号