自拍偷在线精品自拍偷,亚洲欧美中文日韩v在线观看不卡

JavaScript 中如何實(shí)現(xiàn)大文件并發(fā)上傳?

開發(fā) 前端
本文將介紹如何利用 async-pool 這個(gè)庫提供的 asyncPool 函數(shù)來實(shí)現(xiàn)大文件的并發(fā)上傳。

[[402831]]

在 JavaScript 中如何實(shí)現(xiàn)并發(fā)控制? 這篇文章中,阿寶哥詳細(xì)分析了 async-pool 這個(gè)庫如何利用 Promise.all 和 Promise.race 函數(shù)實(shí)現(xiàn)異步任務(wù)的并發(fā)控制。之后,阿寶哥通過 JavaScript 中如何實(shí)現(xiàn)大文件并行下載? 這篇文章介紹了 async-pool 這個(gè)庫的實(shí)際應(yīng)用。

本文將介紹如何利用 async-pool 這個(gè)庫提供的 asyncPool 函數(shù)來實(shí)現(xiàn)大文件的并發(fā)上傳。相信有些小伙伴已經(jīng)了解大文件上傳的解決方案,在上傳大文件時(shí),為了提高上傳的效率,我們一般會(huì)使用 Blob.slice 方法對大文件按照指定的大小進(jìn)行切割,然后通過多線程進(jìn)行分塊上傳,等所有分塊都成功上傳后,再通知服務(wù)端進(jìn)行分塊合并。

看完上圖相信你對大文件上傳的方案,已經(jīng)有了一定的了解。接下來,我們先來介紹 Blob 和 File 對象。

一、Blob 和 File 對象

1.1 Blob 對象

Blob(Binary Large Object)表示二進(jìn)制類型的大對象。在數(shù)據(jù)庫管理系統(tǒng)中,將二進(jìn)制數(shù)據(jù)存儲(chǔ)為一個(gè)單一個(gè)體的集合。Blob 通常是影像、聲音或多媒體文件。在 JavaScript 中 Blob 類型的對象表示不可變的類似文件對象的原始數(shù)據(jù)。 為了更直觀的感受 Blob 對象,我們先來使用 Blob 構(gòu)造函數(shù),創(chuàng)建一個(gè) myBlob 對象,具體如下圖所示:

如你所見,myBlob 對象含有兩個(gè)屬性:size 和 type。其中 size 屬性用于表示數(shù)據(jù)的大小(以字節(jié)為單位),type 是 MIME 類型的字符串。Blob 由一個(gè)可選的字符串 type(通常是 MIME 類型)和 blobParts 組成:

Blob 表示的不一定是 JavaScript 原生格式的數(shù)據(jù)。比如 File 接口基于 Blob,繼承了 Blob 的功能并將其擴(kuò)展使其支持用戶系統(tǒng)上的文件。

1.2 File 對象

通常情況下, File 對象是來自用戶在一個(gè) 元素上選擇文件后返回的 FileList 對象,也可以是來自由拖放操作生成的 DataTransfer 對象,或者來自 HTMLCanvasElement 上的 mozGetAsFile() API。

File 對象是特殊類型的 Blob,且可以用在任意的 Blob 類型的上下文中。比如說 FileReader、URL.createObjectURL() 及 XMLHttpRequest.send() 都能處理 Blob 和 File。在大文件上傳的場景中,我們將使用 Blob.slice 方法對大文件按照指定的大小進(jìn)行切割,然后對分塊進(jìn)行并行上傳。接下來,我們來看一下具體如何實(shí)現(xiàn)大文件上傳。

二、如何實(shí)現(xiàn)大文件上傳

為了讓大家能夠更好地理解后面的內(nèi)容,我們先來看一下整體的流程圖:

了解完大文件上傳的流程之后,我們先來定義上述流程中涉及的一些輔助函數(shù)。

2.1 定義輔助函數(shù)

2.1.1 定義 calcFileMD5 函數(shù)

顧名思義 calcFileMD5 函數(shù),用于計(jì)算文件的 MD5 值(數(shù)字指紋)。在該函數(shù)中,我們使用 FileReader API 分塊讀取文件的內(nèi)容,然后通過 spark-md5 這個(gè)庫提供的方法來計(jì)算文件的 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 函數(shù)

在 JavaScript 中如何實(shí)現(xiàn)并發(fā)控制? 這篇文章中,我們介紹了 asyncPool 函數(shù),它用于實(shí)現(xiàn)異步任務(wù)的并發(fā)控制。該函數(shù)接收 3 個(gè)參數(shù):

  • poolLimit(數(shù)字類型):表示限制的并發(fā)數(shù);
  • array(數(shù)組類型):表示任務(wù)數(shù)組;
  • iteratorFn(函數(shù)類型):表示迭代函數(shù),用于實(shí)現(xiàn)對每個(gè)任務(wù)項(xiàng)進(jìn)行處理,該函數(shù)會(huì)返回一個(gè) Promise 對象或異步函數(shù)。
  1. async function asyncPool(poolLimit, array, iteratorFn) { 
  2.   const ret = []; // 存儲(chǔ)所有的異步任務(wù) 
  3.   const executing = []; // 存儲(chǔ)正在執(zhí)行的異步任務(wù) 
  4.   for (const item of array) { 
  5.     // 調(diào)用iteratorFn函數(shù)創(chuàng)建異步任務(wù) 
  6.     const p = Promise.resolve().then(() => iteratorFn(item, array)); 
  7.     ret.push(p); // 保存新的異步任務(wù) 
  8.  
  9.     // 當(dāng)poolLimit值小于或等于總?cè)蝿?wù)個(gè)數(shù)時(shí),進(jìn)行并發(fā)控制 
  10.     if (poolLimit <= array.length) { 
  11.       // 當(dāng)任務(wù)完成后,從正在執(zhí)行的任務(wù)數(shù)組中移除已完成的任務(wù) 
  12.       const e = p.then(() => executing.splice(executing.indexOf(e), 1)); 
  13.       executing.push(e); // 保存正在執(zhí)行的異步任務(wù) 
  14.       if (executing.length >= poolLimit) { 
  15.         await Promise.race(executing); // 等待較快的任務(wù)執(zhí)行完成 
  16.       } 
  17.     } 
  18.   } 
  19.   return Promise.all(ret); 

2.1.3 定義 checkFileExist 函數(shù)

checkFileExist 函數(shù)用于檢測文件是否已經(jīng)上傳過了,如果已存在則秒傳,否則返回已上傳的分塊 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 函數(shù)中使用到的 request 對象是 Axios 實(shí)例,通過 axios.create方法來創(chuàng)建:

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

有了 request 對象之后,我們就可以輕易地發(fā)送 HTTP 請求。在 checkFileExist 函數(shù)內(nèi)部,我們會(huì)發(fā)起一個(gè) GET 請求,同時(shí)攜帶的查詢參數(shù)是文件名(name)和文件的 MD5 值。

2.1.4 定義 upload 函數(shù)

當(dāng)調(diào)用 checkFileExist 函數(shù)之后,如果發(fā)現(xiàn)文件尚未上傳或者只上傳完部分分塊的話,就會(huì)繼續(xù)調(diào)用 upload 函數(shù)來執(zhí)行上傳任務(wù)。在 upload 函數(shù)內(nèi),我們使用了前面介紹的 asyncPool 函數(shù)來實(shí)現(xiàn)異步任務(wù)的并發(fā)控制,具體如下所示:

  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); // 對文件進(jìn)行切割 
  14.     return uploadChunk({ 
  15.       url, 
  16.       chunk, 
  17.       chunkIndex: i, 
  18.       fileMd5, 
  19.       fileName: file.name
  20.     }); 
  21.   }); 

對于切割完的文件塊,會(huì)通過 uploadChunk 函數(shù),來執(zhí)行實(shí)際的上傳操作:

  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 函數(shù)

當(dāng)所有分塊都上傳完成之后,我們需要通知服務(wù)端執(zhí)行分塊合并操作,這里我們定義了 concatFiles 函數(shù)來實(shí)現(xiàn)該功能:

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

2.1.6 定義 uploadFile 函數(shù)

在前面已定義輔助函數(shù)的基礎(chǔ)上,我們就可以根據(jù)大文件上傳的整體流程圖來實(shí)現(xiàn)一個(gè) uploadFile 函數(shù):

  1. async function uploadFile() { 
  2.   if (!uploadFileEle.files.length) return
  3.   const file = uploadFileEle.files[0]; // 獲取待上傳的文件 
  4.   const fileMd5 = await calcFileMD5(file); // 計(jì)算文件的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, // 限制的并發(fā)數(shù) 
  21.      }); 
  22.   } 
  23.   await concatFiles("/concatFiles", file.name, fileMd5); 

2.2 大文件并發(fā)上傳示例

定義完 uploadFile 函數(shù),要實(shí)現(xiàn)大文件并發(fā)上傳的功能就很簡單了,具體代碼如下所示:

  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>大文件并發(fā)上傳示例(阿寶哥)</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); // 計(jì)算文件的MD5 
  26.         // ... 
  27.       } 
  28.       // 省略其他函數(shù) 
  29.     </script> 
  30.   </body> 
  31. </html> 

由于完整的示例代碼內(nèi)容比較多,阿寶哥就不放具體的代碼了。感興趣的小伙伴,可以訪問以下地址瀏覽客戶端和服務(wù)器端代碼。

  • 完整的示例代碼(代碼僅供參考,可根據(jù)實(shí)際情況進(jìn)行調(diào)整):
  • https://gist.github.com/semlinker/b211c0b148ac9be0ac286b387757e692

最后我們來看一下大文件并發(fā)上傳示例的運(yùn)行結(jié)果:

三、總結(jié)

本文介紹了在 JavaScript 中如何利用 async-pool 這個(gè)庫提供的 asyncPool 函數(shù),來實(shí)現(xiàn)大文件的并發(fā)上傳。此外,文中我們也使用了 spark-md5 這個(gè)庫來計(jì)算文件的數(shù)字指紋,如果你數(shù)字指紋感興趣的話,可以閱讀 數(shù)字指紋有什么用?趕緊來了解一下 這篇文章。

由于篇幅有限,阿寶哥并未介紹服務(wù)端的具體代碼。其實(shí)在做文件分塊合并時(shí),阿寶哥是以流的形式進(jìn)行合并,感興趣的小伙伴可以自行閱讀一下相關(guān)代碼。如果有遇到不清楚的地方,歡迎隨時(shí)跟阿寶哥交流喲。

四、參考資源

  • 你不知道的 Blob
  • MDN - File
  • MDN - ArrayBuffer
  • MDN - HTTP請求范圍
  • JavaScript 中如何實(shí)現(xiàn)并發(fā)控制?

 

責(zé)任編輯:姜華 來源: 全棧修仙之路
相關(guān)推薦

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 前端并發(fā)控制

2010-02-05 08:32:32

ASP.NET MVC

2009-12-07 09:45:23

PHP上傳大文件設(shè)置

2013-03-22 14:42:01

OSS開放存儲(chǔ)服務(wù)云計(jì)算

2024-06-17 09:02:01

2021-01-12 10:22:45

JavaScript并發(fā)控制前端

2022-08-05 08:40:37

架構(gòu)

2009-11-16 11:41:19

PHP上傳大文件

2009-07-21 15:38:31

2021-01-15 11:40:44

文件Java秒傳

2024-07-02 10:18:18

2009-07-20 16:09:39

2009-07-08 09:29:58

WebWork

2025-03-28 05:10:00

Spring上傳大文件

2009-07-21 16:05:58

ASP.NET大文件上

2024-03-27 08:28:31

元素拖拽API文件上傳

2010-09-08 16:50:11

JavaScriptDOM操作
點(diǎn)贊
收藏

51CTO技術(shù)棧公眾號(hào)