<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          一個(gè)企業(yè)級(jí)的文件上傳組件應(yīng)該是什么樣的

          共 13294字,需瀏覽 27分鐘

           ·

          2023-02-10 21:21

          前言

          大廠技術(shù)  高級(jí)前端  Node進(jìn)階

          點(diǎn)擊上方 程序員成長(zhǎng)指北,關(guān)注公眾號(hào)

          回復(fù)1,加入高級(jí)Node交流群

          大家好這里是陽(yáng)九,一個(gè)中途轉(zhuǎn)行的野路子碼農(nóng),熱衷于研究和手寫(xiě)前端工具.

          我的宗旨就是 萬(wàn)物皆可手寫(xiě)

          新手創(chuàng)作不易,有問(wèn)題歡迎指出和輕噴,謝謝

          本文適合有一定node后端基礎(chǔ)的前端同學(xué),如果對(duì)后端完全不了解請(qǐng)惡補(bǔ)前置知識(shí)。

          廢話不多說(shuō),直接進(jìn)入正題。


          我們來(lái)看一下,各個(gè)版本的文件上傳組件大概都長(zhǎng)什么樣

          等級(jí)功能
          青銅-垃圾玩意原生+axios.post
          白銀-體驗(yàn)升級(jí)粘貼,拖拽,進(jìn)度條
          黃金-功能升級(jí)斷點(diǎn)續(xù)傳,秒傳,類(lèi)型判斷
          鉑金-速度升級(jí)web-worker,時(shí)間切片,抽樣hash
          鉆石-網(wǎng)絡(luò)升級(jí)異步并發(fā)數(shù)控制,切片報(bào)錯(cuò)重試
          王者-精雕細(xì)琢慢啟動(dòng)控制,碎片清理等等

          1.最簡(jiǎn)單的文件上傳

          文件上傳,我們需要獲取文件對(duì)象,然后使用formData發(fā)送給后端接收即可

          function upload(file){
              let formData = new FormData();
              formData.append('newFile', file);
              
              axios.post(
              'http://localhost:8000/uploader/upload',
              formData, 
              { headers: { 'Content-Type''multipart/form-data' } }
              )
          }

          復(fù)制代碼

          2.拖拽+粘貼+樣式優(yōu)化

          懶得寫(xiě),你們網(wǎng)上找找?guī)彀?網(wǎng)上啥都有,或者直接組件庫(kù)解決問(wèn)題

          3.斷點(diǎn)續(xù)傳+秒傳+進(jìn)度條

          文件切片

          我們通過(guò)將一個(gè)文件分為多個(gè)小塊,保存到數(shù)組中.逐個(gè)發(fā)送給后端,實(shí)現(xiàn)斷點(diǎn)續(xù)傳。

          image.png
          // 計(jì)算文件hash作為id
          const { hash } = await calculateHashSample(file)
          //todo 生成文件分片列表 
          // 使用file.slice()將文件切片
          const fileList = [];
          const count = Math.ceil(file.size / globalProp.SIZE);
          const partSize = file.size / count;
          let cur = 0  // 記錄當(dāng)前切片的位置
          for (let i = 0; i < count; i++) {

              let item = { 
              chunk: file.slice(cur, cur + partSize), 
              filename`${hash}_${i}`
              };
              
              fileList.push(item);
          }
          復(fù)制代碼

          計(jì)算hash

          為了讓后端知道,這個(gè)切片是某個(gè)文件的一部分,以便聚合成一個(gè)完整的文件。我們需要計(jì)算完整file的唯一值(md5),作為切片的文件名。

          // 通過(guò)input的event獲取到file
          <input type="file" @change="getFile">
          // 使用SparkMD5計(jì)算文件hash,讀取文件為blob,計(jì)算hash
          let fileReader = new FileReader();

          fileReader.onload = (e)=>{
          let hexHash = SparkMD5.hash(e.target.result)
          console.log(hexHash); 
          };
          復(fù)制代碼

          斷點(diǎn)續(xù)傳+秒傳(前端)

          我們此時(shí)有保存了100個(gè)文件切片的數(shù)組,遍歷切片連續(xù)向后端發(fā)送axios.post請(qǐng)求即可 設(shè)置一個(gè)開(kāi)關(guān),實(shí)現(xiàn)啟動(dòng)-暫停功能。

          如果我們傳了50份,關(guān)掉了瀏覽器怎么辦?

          此時(shí)我們需要后端配合,在上傳文件之前,先檢查一下后端接收了多少文件。

          當(dāng)然,如果發(fā)現(xiàn)后端已經(jīng)上傳過(guò)這個(gè)文件,直接顯示上傳完畢(秒傳)

          // 解構(gòu)出已經(jīng)上傳的文件數(shù)組 文件是否已經(jīng)上傳完畢 
          // 通過(guò)文件hash和后綴查詢(xún)當(dāng)前文件有多少已經(jīng)上傳的部分
          const {isFileUploaded, uploadedList} = await axios.get(
          `http://localhost:8000/uploader/count 
          ?hash=${hash}         
          &suffix=${fileSuffix}
          `
          )
          復(fù)制代碼

          斷點(diǎn)續(xù)傳+秒傳(后端)

          至于后端的操作,就比較簡(jiǎn)單了

          1. 根據(jù)文件hash創(chuàng)建文件夾,保存文件切片
          2. 檢查某文件的上傳情況,通過(guò)接口返回給前端

          例如以下文件切片文件夾

          image.png
          //! --------通過(guò)hash查詢(xún)服務(wù)器中已經(jīng)存放了多少份文件(或者是否已經(jīng)存在文件)------
          function checkChunks(hash, suffix
          //! 查看已經(jīng)存在多少文件 獲取已上傳的indexList 
          const chunksPath = `${uploadChunksDir}${hash}`;
          const chunksList = (fs.existsSync(chunksPath) && fs.readdirSync(chunksPath)) || []; 
          const indexList = chunksList.map((item, index) =>item.split('_')[1]) 
          //! 通過(guò)查詢(xún)文件hash+suffix 判斷文件是否已經(jīng)上傳 
          const filename = `${hash}${suffix}`
          const fileList = (fs.existsSync(uploadFileDir) && fs.readdirSync(uploadFileDir)) || []; 
          const isFileUploaded = fileList.indexOf(filename) === -1 ? false : true 

          console.log('已經(jīng)上傳的chunks', chunksList.length); 
          console.log('文件是否存在', isFileUploaded); 

          return { 
              code: 200,
              data: { 
                  count: chunksList.length, 
                  uploadedList: indexList, 
                  isFileUploaded: isFileUploaded
                  }
                }
              }
          復(fù)制代碼

          進(jìn)度條

          實(shí)時(shí)計(jì)算一下已經(jīng)成功上傳的片段不就行了,自行實(shí)現(xiàn)吧

          4.抽樣hash和webWorker

          因?yàn)樯蟼髑?我們需要計(jì)算文件的md5值,作為切片的id使用。

          md5的計(jì)算是一個(gè)非常耗時(shí)的事情,如果文件過(guò)大,js會(huì)卡在計(jì)算md5這一步,造成頁(yè)面長(zhǎng)時(shí)間卡頓。

          我們這里提供三種思路進(jìn)行優(yōu)化

          抽樣hash(md5)

          抽樣hash是指,我們截取整個(gè)文件的一部分,計(jì)算hash,提升計(jì)算速度.

          1. 我們將file解析為二進(jìn)制buffer數(shù)據(jù),

          2. 抽取文件頭尾2mb, 中間的部分每隔2mb抽取2kb

          3. 將這些片段組合成新的buffer,進(jìn)行md5計(jì)算。

          圖解:

          image.png

          樣例代碼

          //! ---------------抽樣md5計(jì)算-------------------
          function calculateHashSample(file{

              return new Promise((resolve) => {
                  //!轉(zhuǎn)換文件類(lèi)型(解析為BUFFER數(shù)據(jù) 用于計(jì)算md5)
                  const spark = new SparkMD5.ArrayBuffer();
                  const { size } = file;
                  const OFFSET = Math.floor(2 * 1024 * 1024); // 取樣范圍 2M
                  const reader = new FileReader();
                  let index = OFFSET;
                  // 頭尾全取,中間抽2字節(jié)
                  const chunks = [file.slice(0, index)];
                  while (index < size) {
                      if (index + OFFSET > size) {
                          chunks.push(file.slice(index));
                      } else {
                          const CHUNK_OFFSET = 2;
                          chunks.push(file.slice(index, index + 2),
                              file.slice(index + OFFSET - CHUNK_OFFSET, index + OFFSET));
                      }
                      index += OFFSET;
                  }
                  // 將抽樣后的片段添加到spark
                  reader.onload = (event) => {
                      spark.append(event.target.result);
                      resolve({
                          hash: spark.end(),//Promise返回hash
                      });
                  }
                  reader.readAsArrayBuffer(new Blob(chunks));
              });
          }
          復(fù)制代碼

          webWorker

          除了抽樣hash,我們可以另外開(kāi)啟一個(gè)webWorker線程去專(zhuān)門(mén)計(jì)算md5.

          webWorker: 就是給JS創(chuàng)造多線程運(yùn)行環(huán)境,允許主線程創(chuàng)建worker線程,分配任務(wù)給后者,主線程運(yùn)行的同時(shí)worker線程也在運(yùn)行,相互不干擾,在worker線程運(yùn)行結(jié)束后把結(jié)果返回給主線程。

          具體使用方式可以參考MDN或者其他文章

          使用 Web Workers \- Web API 接口參考 | MDN \(mozilla.org\)[1]

          一文徹底學(xué)會(huì)使用web worker \- 掘金 \(juejin.cn\)[2]

          時(shí)間切片

          熟悉React時(shí)間切片的同學(xué)也可以去試一試,不過(guò)個(gè)人認(rèn)為這個(gè)方案沒(méi)有以上兩種好。

          不熟悉的同學(xué)可以自行掘金一下,文章還是很多的。

          這里就不多做論述,只提供思路

          時(shí)間切片也就是傳說(shuō)中的requestIdleCallback,requestAnimationCallback 這兩個(gè)API了,或者高級(jí)一點(diǎn)自己通過(guò)messageChannel去封裝。

          切片計(jì)算hash,將多個(gè)短任務(wù)分布在每一幀里,減少頁(yè)面卡頓。

          5.文件類(lèi)型判斷

          • 簡(jiǎn)單一點(diǎn),我們可以通過(guò)input標(biāo)簽的accept屬性,或者截取文件名來(lái)判斷類(lèi)型
          <input id="file" type="file" accept="image/*" />

          const ext = file.name.substring(file.name.lastIndexOf('.') + 1);
          復(fù)制代碼

          當(dāng)然這種限制可以簡(jiǎn)單的通過(guò)修改文件后綴名來(lái)突破,并不嚴(yán)謹(jǐn)。

          • 通過(guò)文件頭判斷文件類(lèi)型

          我們將文件轉(zhuǎn)化為二進(jìn)制blob,文件的前幾個(gè)字節(jié)就表示了文件類(lèi)型,我們讀取進(jìn)行判斷即可。

          比如如下代碼

          // 判斷是否為 .jpg 
          async function isJpg(file{
            // 截取前幾個(gè)字節(jié),轉(zhuǎn)換為string
            const res = await blobToString(file.slice(03))
            return res === 'FF D8 FF'
          }
          // 判斷是否為 .png 
          async function isPng(file{
            const res = await blobToString(file.slice(04))
            return res === '89 50 4E 47'
          }
          // 判斷是否為 .gif 
          async function isGif(file{
            const res = await blobToString(file.slice(04))
            return res === '47 49 46 38'
          }
          復(fù)制代碼

          當(dāng)然咱們有現(xiàn)成的庫(kù)可以做這件事情,比如 file-type 這個(gè)庫(kù)

          file-type \- npm \(npmjs.com\)[3]

          6.異步并發(fā)數(shù)控制(重要)

          我們需要將多個(gè)文件片段上傳給后端,總不能一個(gè)個(gè)發(fā)送把?我們這里使用TCP的并發(fā)+實(shí)現(xiàn)控制并發(fā)進(jìn)行上傳。

          image.png
          1. 首先我們將100個(gè)文件片段都封裝為axios.post函數(shù),存入任務(wù)池中
          2. 創(chuàng)建一個(gè)并發(fā)池,同時(shí)執(zhí)行并發(fā)池中的任務(wù),發(fā)送片段
          3. 設(shè)置計(jì)數(shù)器i,當(dāng)i<并發(fā)數(shù)時(shí),才能將任務(wù)推入并發(fā)池
          4. 通過(guò)promise.race方法 最先執(zhí)行完畢的請(qǐng)求會(huì)被返回 即可調(diào)用其.then方法 傳入下一個(gè)請(qǐng)求(遞歸)
          5. 當(dāng)最后一個(gè)請(qǐng)求發(fā)送完畢 向后端發(fā)起請(qǐng)求 合并文件片段

          圖解

          image.png

          代碼

          //! 傳入請(qǐng)求列表  最大并發(fā)數(shù)  全部請(qǐng)求完畢后的回調(diào)
          function concurrentSendRequest(requestArr: any, max = 3, callback: any{
              let i = 0 // 執(zhí)行任務(wù)計(jì)數(shù)器
              let concurrentRequestArr: any[] = [] //并發(fā)請(qǐng)求列表

              let toFetch: any = () => {
                  // (每次執(zhí)行i+1) 如果i=arr.length 說(shuō)明是最后一個(gè)任務(wù)  
                  // 返回一個(gè)resolve 作為最后的toFetch.then()執(zhí)行
                  // (執(zhí)行Promise.all() 全部任務(wù)執(zhí)行完后執(zhí)行回調(diào)函數(shù)  發(fā)起文件合并請(qǐng)求)
                  if (i === requestArr.length) {
                      return Promise.resolve()
                  }

                  //TODO 執(zhí)行異步任務(wù)  并推入并發(fā)列表(計(jì)數(shù)器+1)
                  let it = requestArr[i++]()
                  concurrentRequestArr.push(it)

                  //TODO 任務(wù)執(zhí)行后  從并發(fā)列表中刪除
                  it.then(() => {
                      concurrentRequestArr.splice(concurrentRequestArr.indexOf(it), 1)
                  })

                  //todo 如果并發(fā)數(shù)達(dá)到最大數(shù),則等其中一個(gè)異步任務(wù)完成再添加
                  let p = Promise.resolve()
                  if (concurrentRequestArr.length >= max) {
                      //! race方法 返回fetchArr中最快執(zhí)行的任務(wù)結(jié)果 
                      p = Promise.race(concurrentRequestArr)
                  }
                  //todo race中最快完成的promise,在其.then遞歸toFetch函數(shù)
                  if (globalProp.stop) { return p.then(() => { console.log('停止發(fā)送') }) }
                  return p.then(() => toFetch())
              }

              // 最后一組任務(wù)全部執(zhí)行完再執(zhí)行回調(diào)函數(shù)(發(fā)起合并請(qǐng)求)(如果未合并且未暫停)
              toFetch().then(() =>
                  Promise.all(concurrentRequestArr).then(() => {
                      if (!globalProp.stop && !globalProp.finished) { callback() }
                  })
              )
          }
          復(fù)制代碼

          7.并發(fā)錯(cuò)誤重試

          1. 使用catch捕獲任務(wù)錯(cuò)誤,上述axios.post任務(wù)執(zhí)行失敗后,重新把任務(wù)放到任務(wù)隊(duì)列中
          2. 給每個(gè)任務(wù)對(duì)象設(shè)置一個(gè)tag,記錄任務(wù)重試的次數(shù)
          3. 如果一個(gè)切片任務(wù)出錯(cuò)超過(guò)3次,直接reject。并且可以直接終止文件傳輸

          8.慢啟動(dòng)控制

          由于文件大小不一,我們每個(gè)切片的大小設(shè)置成固定的也有點(diǎn)略顯笨拙,我們可以參考TCP協(xié)議的慢啟動(dòng)策略,

          . 設(shè)置一個(gè)初始大小,根據(jù)上傳任務(wù)完成的時(shí)候,來(lái)動(dòng)態(tài)調(diào)整下一個(gè)切片的大小, 確保文件切片的大小和當(dāng)前網(wǎng)速匹配

          1. chunk中帶上size值,不過(guò)進(jìn)度條數(shù)量不確定了,修改createFileChunk, 請(qǐng)求加上時(shí)間統(tǒng)計(jì)
          2. 比如我們理想是30秒傳遞一個(gè)
          3. 初始大小定為1M,如果上傳花了10秒,那下一個(gè)區(qū)塊大小變成3M
          4. 如果上傳花了60秒,那下一個(gè)區(qū)塊大小變成500KB 以此類(lèi)推

          9.碎片清理

          如果用戶上傳文件到一半終止,并且以后也不傳了,后端保存的文件片段也就沒(méi)有用了。

          我們可以在node端設(shè)置一個(gè)定時(shí)任務(wù)setInterval,每隔一段時(shí)間檢查并清理不需要的碎片文件

          可以使用 node-schedule 來(lái)管理定時(shí)任務(wù),比如每天檢查一次目錄,如果文件是一個(gè)月前的,那就直接刪除把。

          垃圾碎片文件

          后記

          以上就是一個(gè)完整的比較高級(jí)的文件上傳組件的全部功能,希望各位有耐心看到這里的新手小伙伴能夠融會(huì)貫通。每天進(jìn)步一點(diǎn)點(diǎn)。

          參考資料

          [1]

          https://developer.mozilla.org/zh-CN/docs/Web/API/Web_Workers_API/Using_web_workers: https://link.juejin.cn?target=https%3A%2F%2Fdeveloper.mozilla.org%2Fzh-CN%2Fdocs%2FWeb%2FAPI%2FWeb_Workers_API%2FUsing_web_workers

          [2]

          https://juejin.cn/post/7139718200177983524: https://juejin.cn/post/7139718200177983524

          [3]

          https://www.npmjs.com/package/file-type: https://link.juejin.cn?target=https%3A%2F%2Fwww.npmjs.com%2Fpackage%2Ffile-type

          - END -

          關(guān)于本文

          作者:不月陽(yáng)九

          https://juejin.cn/post/7187695113597321275

          Node 社群


          我組建了一個(gè)氛圍特別好的 Node.js 社群,里面有很多 Node.js小伙伴,如果你對(duì)Node.js學(xué)習(xí)感興趣的話(后續(xù)有計(jì)劃也可以),我們可以一起進(jìn)行Node.js相關(guān)的交流、學(xué)習(xí)、共建。下方加 考拉 好友回復(fù)「Node」即可。

             “分享、點(diǎn)贊在看” 支持一波??

          瀏覽 19
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          評(píng)論
          圖片
          表情
          推薦
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  人人干人人摸人人操 | 影音先锋啪啪啪 | 久久伊人777777 | 五月婷婷在线视频 | 欧美亚黄片 |