vue實(shí)現(xiàn)大文件切片上傳论矾、斷點(diǎn)續(xù)傳、并發(fā)數(shù)控制等

最近在學(xué)習(xí)文件上傳相關(guān)知識杆勇,記錄下整體流程

頁面效果
一贪壳、上傳按鈕和進(jìn)度條等
<div>
  <h2>上傳文件</h2>
  <div ref="drag" class="drag">
    <input class="file" type="file" @change="handlerChange" />
  </div>
  <el-progress style="width: 500px;" :percentage="progress"></el-progress>
  <div style="margin-top: 16px;">
    <el-button type="primary" @click="upload">上傳</el-button>
  </div>
  <div>
    <p>hash進(jìn)度條</p>
    <el-progress style="width: 500px;" :percentage="hashProgress"></el-progress>
  </div>
  <div>
    <p>網(wǎng)格進(jìn)度條</p>
    <ul class="grid" :style="{'width': gridWidth + 'px'}">
      <li class="grid-block" v-for="chunk in chunks" :key="chunk.name">
        <div 
          :class="{ 'uploading': chunk.progress > 0 && chunk.progress < 100, 'success': chunk.progress == 100, 'error': chunk.progress < 0}"
          :style="{height: chunk.progress + '%'}"
        >
          <i class="el-icon-loading" style="color: #f56c6c" v-if="chunk.progress < 100 && chunk.progress > 0"></i>
        </div>
      </li>
    </ul>
  </div>
</div>
二、選擇文件
//點(diǎn)擊按鈕上傳
handlerChange (e) {
  const [file] = e.target.files
  if (!file) return
  this.fileData = file
}
//拖拽上傳
dragRelevant () {
  const dragDom = this.$refs.drag
  //進(jìn)入?yún)^(qū)域
  dragDom.addEventListener('dragover', e => {
    dragDom.style.borderColor = '#f00'
    e.preventDefault()
  })
  //離開區(qū)域
  dragDom.addEventListener('dragleave', e => {
    dragDom.style.borderColor = '#41B883'
    e.preventDefault()
  })
  //放下文件
  dragDom.addEventListener('drop', e => {
    dragDom.style.borderColor = '#41B883'
    const [file] = e.dataTransfer.files
    if (!file) return
    this.fileData = file
    e.preventDefault()
  })
}
三蚜退、利用文件內(nèi)容計算hash

為了防止文件上傳重復(fù)寥袭,我們可以使用將每個文件都用hash作為文件名來上傳,這里用的是spark-md5來計算hash值关霸。
首先定一個分塊的大小

const CHUNK_SIZE = 1 * 1024 * 1024 //每次分片大小

因?yàn)榇笪募谜麄€內(nèi)容來計算hash肯定是很慢的传黄,我們不能阻塞頁面執(zhí)行其他任務(wù),所以我通過下面三種方式來計算:

  • 使用WebWorker來計算
//使用webWorker來計算文件的md5值
calculateHashByWebWorker (chunks) {
  this.hashProgress = 0 //hash進(jìn)度條
  return new Promise(resolve => {
    const worker = new Worker('/hash.js')
    worker.postMessage(chunks)
    worker.onmessage = e => {
      const { hash, progress } = e.data
      this.hashProgress = progress
      if (hash) {
        resolve(hash)
      }
    }
  })
}
  • 使用requestIdleCallbck來計算
//使用requestIdleCallbck來計算文件的md5值  這個方法會在瀏覽器空閑時調(diào)用
calculateHashByRequestIdleCallback (chunks) {
  return new Promise(resolve => {
    const spark = new Spark.ArrayBuffer()
    let count = 0
    const appendToSpark = file => {
      return new Promise(resolve => {
        const reader = new FileReader()
        reader.readAsArrayBuffer(file)
        reader.onload = data => {
          spark.append(data.target.result)
          resolve()
        }
      })
    }
    const workLoop = async deadLine => {
      while (count < chunks.length && deadLine.timeRemaining() > 1) {
        await appendToSpark(chunks[count].file)
        count++
        if (count < chunks.length) {
          this.hashProgress = (count * 100 / chunks.length).toFixed(2) - 0
        } else {
          this.hashProgress = 100
          resolve(spark.end())
        }
      }
      window.requestIdleCallback(workLoop)
    }
    window.requestIdleCallback(workLoop)
  })
}
  • 實(shí)現(xiàn)抽樣hash队寇,降低精度膘掰,提高效率
    大文件每次都全量計算md5的話,效率很低佳遣,如果我們每次取每個分片的一部分用來計算识埋,這樣會大大提高計算的效率
//抽樣hash 取前兩個和后一個  中間每兆取前中后三個點(diǎn)
calulateSamplingHash (chunks) {
  return new Promise(resolve => {
    const spark = new Spark.ArrayBuffer()
    const head = chunks.slice(0, 2)
    const tail = chunks[chunks.length - 1]
    const middle = chunks.slice(2, chunks.length - 1)
    const files = []
    files.push(head[0].file, head[1].file)
    middle.forEach(item => {
      const head = item.file.slice(0, 1)
      const tail = item.file.slice(-1, item.file.length)
      const center = Math.floor(item.file.length - 1) / 2
      const middle = item.file.slice(center, center + 1)
      files.push(head, tail, middle)
    })
    files.push(tail.file)
    //追加計算hash
    const reader = new FileReader()
    reader.readAsArrayBuffer(new Blob(files))
    reader.onload = data => {
      spark.append(data.target.result)
      this.hashProgress = 100
      resolve(spark.end())
    }
  })
}
四、上傳

將上傳的諸多分片都放在對應(yīng)hash值得目錄下面零渐,每次上傳前檢查下是否有這個文件了
如果有就提示秒傳成功
如果沒有就讀取下這個目錄窒舟,將這個目錄下面的所有文件名都返回給前端

  • 檢查文件是否已上傳
//檢查文件是否已上傳
const fileExt = this.fileData.name.split('.').pop()
// uploaded:文件是否已上傳,uploadedList:上傳的分片列表
const { data: { uploaded, uploadedList } } = await this.$axios.get('/checkFile', {
  params: {
    hash,
    ext: fileExt
  }
})
if (uploaded) {
  this.$message.success('秒傳成功')
  return
}
//斷點(diǎn)續(xù)傳  根據(jù)之前上傳的文件
this.chunks = chunks.map((chunk, index) => {
  const fileName = `${hash}-${index}`
  return {
    file: new File([chunk.file], fileName + '.' + fileExt, { type: 'image/mp4' }),
    name: fileName,
    hash,
    progress: uploadedList.includes(fileName) ? 100 : 0 //如果當(dāng)前分片已經(jīng)上傳诵盼,進(jìn)度直接設(shè)置為100
  }
})
  • 上傳請求(斷點(diǎn)續(xù)傳)
//上傳請求
async uploadRequest (hash) {
  //如果已經(jīng)上傳過了 就不用上傳了  用filter過濾掉(斷點(diǎn)續(xù)傳)
  const requests = this.chunks.map((chunk, index) => {
    if (chunk.progress === 100) {
      return null
    } else {
      const form = new FormData()
      form.append('chunk', chunk.file)
      form.append('hash', chunk.hash)
      form.append('name', chunk.name)
      return { form, index, error: 0 }
    }
  }).filter(val => val)
  //實(shí)現(xiàn)并發(fā)數(shù)控制
  await this.sendRequest(requests)
  //合并上傳的分片
  this.mergeFile(hash)
}
  • 并發(fā)數(shù)控制+錯誤重試
//請求并發(fā)數(shù)控制
sendRequest (requests, limit = 3) {
  return new Promise((resolve, reject) => {
    const len = requests.length
    let counter = 0
    let isStop = false //如果一個片段失敗超過三次 認(rèn)為當(dāng)前網(wǎng)洛有問題 停止全部上傳
    const startRequest = async () => {
      if (isStop) return
      const task = requests.shift()
      if (task) {
        //利用try...catch捕獲錯誤
        try {
          //具體的接口  抽離出去了
          await this.launchRequest(task)
          if (counter === len - 1) { //最后一個任務(wù)
            resolve()
          } else { //否則接著執(zhí)行
            counter++
            startRequest() //啟動下一個任務(wù)
          }
        } catch (error) {
          this.$set(this.chunks[task.index], 'progress', -1)
          //接口報錯重試惠豺,限制為3次
          if (task.error < 3) {
            task.error++
            requests.unshift(task)
            startRequest()
          } else {
            isStop = true
            reject(error)
          }
        }
      }
    }
    //啟動任務(wù)
    while (limit > 0) {
      //模擬不同大小啟動
      setTimeout(() => {
        startRequest()
      }, Math.random() * 2000)
      limit--
    }
  })
}

完整代碼地址(file-vue银还、file-node)

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市洁墙,隨后出現(xiàn)的幾起案子蛹疯,更是在濱河造成了極大的恐慌,老刑警劉巖热监,帶你破解...
    沈念sama閱讀 222,252評論 6 516
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件捺弦,死亡現(xiàn)場離奇詭異,居然都是意外死亡孝扛,警方通過查閱死者的電腦和手機(jī)列吼,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,886評論 3 399
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來苦始,“玉大人寞钥,你說我怎么就攤上這事∮颍” “怎么了?”我有些...
    開封第一講書人閱讀 168,814評論 0 361
  • 文/不壞的土叔 我叫張陵太示,是天一觀的道長柠贤。 經(jīng)常有香客問我,道長类缤,這世上最難降的妖魔是什么臼勉? 我笑而不...
    開封第一講書人閱讀 59,869評論 1 299
  • 正文 為了忘掉前任,我火速辦了婚禮餐弱,結(jié)果婚禮上宴霸,老公的妹妹穿的比我還像新娘。我一直安慰自己膏蚓,他們只是感情好瓢谢,可當(dāng)我...
    茶點(diǎn)故事閱讀 68,888評論 6 398
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著驮瞧,像睡著了一般氓扛。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上论笔,一...
    開封第一講書人閱讀 52,475評論 1 312
  • 那天采郎,我揣著相機(jī)與錄音,去河邊找鬼狂魔。 笑死蒜埋,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的最楷。 我是一名探鬼主播整份,決...
    沈念sama閱讀 41,010評論 3 422
  • 文/蒼蘭香墨 我猛地睜開眼待错,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了皂林?” 一聲冷哼從身側(cè)響起朗鸠,我...
    開封第一講書人閱讀 39,924評論 0 277
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎础倍,沒想到半個月后烛占,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 46,469評論 1 319
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡沟启,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,552評論 3 342
  • 正文 我和宋清朗相戀三年忆家,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片德迹。...
    茶點(diǎn)故事閱讀 40,680評論 1 353
  • 序言:一個原本活蹦亂跳的男人離奇死亡芽卿,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出胳搞,到底是詐尸還是另有隱情卸例,我是刑警寧澤,帶...
    沈念sama閱讀 36,362評論 5 351
  • 正文 年R本政府宣布肌毅,位于F島的核電站筷转,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏悬而。R本人自食惡果不足惜呜舒,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 42,037評論 3 335
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望笨奠。 院中可真熱鬧袭蝗,春花似錦、人聲如沸般婆。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,519評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽蔚袍。三九已至左电,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間页响,已是汗流浹背篓足。 一陣腳步聲響...
    開封第一講書人閱讀 33,621評論 1 274
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留闰蚕,地道東北人栈拖。 一個月前我還...
    沈念sama閱讀 49,099評論 3 378
  • 正文 我出身青樓,卻偏偏與公主長得像没陡,于是被迫代替她去往敵國和親涩哟。 傳聞我的和親對象是個殘疾皇子索赏,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,691評論 2 361

推薦閱讀更多精彩內(nèi)容