1. 程式人生 > 程式設計 >基於vue-simple-uploader封裝檔案分片上傳、秒傳及斷點續傳的全域性上傳外掛功能

基於vue-simple-uploader封裝檔案分片上傳、秒傳及斷點續傳的全域性上傳外掛功能

1. 前言

之前公司要在管理系統中做一個全域性上傳外掛,即切換各個頁面的時候,上傳介面還在並且上傳不會受到影響,這在vue這種spa框架面前並不是什麼難題。然而後端大佬說我們要實現分片上傳、秒傳以及斷點續傳的功能,聽起來頭都大了。

很久之前我寫了一篇webuploader的文章,結果使用起來發現問題很多,且官www.cppcns.com方團隊不再維護這個外掛了, 經過多天調研及踩雷,最終決定基於vue-simple-uploader外掛實現該功能,在專案中使用起來無痛且穩定。

如果你只是想實現基本的(非定製化的)上傳功能,直接使用vue-simple-uploader,多讀一下它的文件,不需要更多的二次封裝。

如果你只是想實現全域性上傳外掛,也可以參照一下我的實現。
如果你用到了分片上傳、秒傳及斷點續傳這些複雜的功能,恭喜你,這篇文章的重點就在於此。

本文原始碼在此:https://github.com/shady-xia/Blog/tree/master/vue-simple-uploader

基於vue-simple-uploader封裝檔案分片上傳、秒傳及斷點續傳的全域性上傳外掛功能

2. 關於vue-simple-uploader

vue-simple-uploader是基於 simple-uploader.js 封裝的vue上傳外掛。它的優點包括且不限於以下幾種:

  • 支援檔案、多檔案、資料夾上傳;支援拖拽檔案、資料夾上傳
  • 可暫停、繼續上傳
  • 錯誤處理
  • 支援“秒傳”,通過檔案判斷服務端是否已存在從而實現“秒傳”
  • 分塊上傳
  • 支援進度、預估剩餘時間、出錯自動重試、重傳等操作

讀這篇文章之前,建議先讀一遍simple-uploader.js的文件,然後再讀一下vue-simple-uploader的文件,瞭解一下各個引數的作用是什麼,我在這裡假定大家已經比較熟悉了。。
vue-simple-uploader文件

simple-uploader.js文件

安裝:npm install vue-simple-uploader --save
使用:在main.js中:

import uploader from 'vue-simple-uploader'
Vue.use(uploader)

3. 基於vue-simple-uploader封裝全域性上傳元件

引入vue-simple-uploader後,我們開始封裝全域性的上傳元件globalUploader.vue,程式碼比較長,就不整個放出來了,原始碼放到github上了,這裡一步一步地講解。

template部分如下,本人自定義了模板和樣式,所以html部分比較長,css部分暫時不列出,大家可以根據自己的ui去更改,主要關注一下uploader這個元件的options引數及檔案addedsuccessprogresserror幾個事件:

<template>
 <div id="global-uploader">

 <!-- 上傳 -->
 <uploader
  ref="uploader"
  :options="options"
  :autoStart="false"
  @file-added="onFileAdded"
  @file-success="onFileSuccess"
  @file-progress="onFileProgress"
  @file-error="onFileError"
  class="uploader-app">
  <uploader-unsupport></uploader-unsupport>

  <uploader-btn id="global-uploader-btn" :attrs="attrs" ref="uploadBtn">選擇檔案</uploader-btn>

  <uploader-list v-show="panelShow">
  <div class="file-panel" slot-scope="props" :class="{'collapse': collapse}">
   <div class="file-title">
   <h2>檔案列表</h2>
   <div class="operate">
    <el-button @click="fileListShow" type="text" :title="collapse ? '展開':'摺疊' ">
    <i class="iconfont" :class="collapse ? 'icon-fullscreen': 'icon-minus-round'"></i>
    </el-button>
    <el-button @click="close" type="text" title="關閉">
    <i class="iconfont icon-close"></i>
    </el-button>
   </div>
   </div>

   <ul class="file-list">
   <li v-for="file in props.fileList" :key="file.id">
    <uploader-file :class="'file_' + file.id" ref="files" :file="file" :list="true"></uploader-file>
   </li>
   <div class="no-file" v-if="!props.fileList.length"><i class="nucfont inuc-empty-file"></i> 暫無待上傳檔案</div>
   </ul>
  </div>
  </uploader-list>

 </uploader>

 </div>
</template>

元件中的data部分:

data() {
 return {
 options: {
  target: 'http://xxxxx/xx',// 目標上傳 URL
  chunkSize: '2048000',//分塊大小
  fileParameterName: 'file',//上傳檔案時檔案的引數名,預設file
  maxChunkRetries: 3,//最大自動失敗重試上傳次數
  testChunks: true,//是否開啟伺服器分片校驗
  // 伺服器分片校驗函式,秒傳及斷點續傳基礎
  checkChunkUploadedByResponse: function (chunk,message) {
  let objMessage = JSON.parse(message);
  if (objMessage.skipUpload) {
   return true;
  }

  return (objMessage.uploaded || []).indexOf(chunk.offset + 1) >= 0
  },headers: {
  // 在header中新增的驗證,請根據實際業務來
  Authorization: "Bearer " + Ticket.get().access_token
  },},attrs: {
  // 接受的檔案型別,形如['.png','.jpg','.jpeg','.gif','.bmp'...] 這裡我封裝了一下
  accept: ACCEPT_C程式設計客棧ONFIG.getAll()
 },panelShow: false,//選擇檔案後,展示上傳panel
 }
},

全域性引用:
app.vue中引用,即作為全域性的元件一直存在,只不過在不使用的時候把上傳介面隱藏了

<global-uploader></global-uploader>

4. 檔案上傳流程概覽

1. 點選按鈕,觸發檔案上傳操作:

(如果你做的不是全域性上傳的功能,而是直接點選上傳,忽略這一步。)

因為我做的是全域性上傳的外掛,要先把上傳的視窗隱藏起來,在點選某個上傳按鈕的時候,用Bus傳送一個openUploader的事件,在globalUploader.vue中接收該事件,trigger我們uploader-btn的click事件。

在某個頁面中,點選上傳按鈕,同時把要給後臺的引數帶過來(如果有的話),這裡元件之間傳值我用的event bus,當然用vuex會更好:

Bus.$emit('openUploader',{
 superiorID: this.superiorID
})

globalUploader.vue中接收該事件:

Bus.$on('openUploader',query => {
 this.params = query || {};

 if (this.$refs.uploadBtn) {
	 // 這樣就打開了選擇檔案的操作視窗
 $('#global-uploader-btn').click();
 }
});

2. 選擇檔案後,將上傳的視窗展示出來,開始md5的計算工作

onFileAdded(file) {
 this.panelShow = true;
	
	// 計算MD5,下文會提到
 this.computeMD5(file);
},

這裡有個前提,我在uploader中將autoStart設為了false,為什麼要這麼做?

在選擇檔案之後,我要計算MD5,以此來實現斷點續傳及秒傳的功能,所以選擇檔案後直接開始上傳肯定不行,要等MD5計算完畢之後,再開始檔案上傳的操作。

具體的MD5計算方法,會在下面講,這裡先簡單引出。

上傳過程中,會不斷觸發file-progress上傳進度的回撥

// 檔案進度的回撥
onFileProgress(rootFile,file,chunk) {
 console.log(`上傳中 ${file.name},chunk:${chunk.startByte / 1024 / 1024} ~ ${chunk.endByte / 1024 / 1024}`)
},

3. 檔案上傳成功後

檔案上傳成功後,在“上傳完成”的回撥中,通過服務端返回的needMerge欄位,來判斷是否需要再發送合併分片的請求,
如果這個欄位為true,則需要給後臺發一個請求合併的ajax請求,否則直接上傳成功。

注意:這裡的needMerge是我和後臺商議決定的欄位名

onFileSuccess(rootFile,response,chunk) {
 let res = JSON.parse(response);

 // 伺服器自定義的錯誤,這種錯誤是Uploader無法攔截的
 if (!res.result) {
 this.$message({ message: res.message,type: 'error' });
 return
 }
	
	// 如果服務端返回需要合併
 if (res.needMerge) {
 api.mergeSimpleUpload({
  tempName: res.tempName,fileName: file.name,...this.params,}).then(data => {
  // 檔案合併成功
  Bus.$emit('fileSuccess',data);
 }).catch(e => {});
 // 不需要合併 
 } else {
 Bus.$emit('fileSuccess',res);
 console.log('上傳成功');
 }
},onFileError(rootFile,chunk) {
	console.log(error)
},

5. 檔案分片

vue-simple-uploader自動將檔案進行分片,在optionschunkSize中可以設定每個分片的大小。

如圖:對於大檔案來說,會發送多個請求,在設定testChunkstrue後(在外掛中預設就是true),會發送與伺服器進行分片校驗的請求,下面的第一個get請求就是該請求;後面的每一個post請求都是上傳分片的請求

基於vue-simple-uploader封裝檔案分片上傳、秒傳及斷點續傳的全域性上傳外掛功能

看一下發送給服務端的引數,其中chunkNumber表示當前是第幾個分片,totalChunks代表所有的分片數,這兩個引數都是都是外掛根據你設定的chunkSize來計算的。

基於vue-simple-uploader封裝檔案分片上傳、秒傳及斷點續傳的全域性上傳外掛功能

需要注意的就是在最後檔案上傳成功的事件中,通過後臺返回的欄位,來判斷是否要再給後臺傳送一個檔案合併的請求。

6. MD5的計算過程

斷點續傳及秒傳的基礎是要計算檔案的MD5,這是檔案的唯一標識,然後伺服器根據MD5進行判斷,是進行秒傳還是斷點續傳。

file-added事件之後,就計算MD5,我們最終的目的是將計算出來的MD5加到引數裡傳給後臺,然後繼續檔案上傳的操作,詳細的思路步驟是:

  • 把uploader元件的autoStart設為false,即選擇檔案後不會自動開始上傳
  • 先通過 file.pause()暫停檔案,然後通過H5的FileReader介面讀取檔案
  • 將非同步讀取檔案的結果進行MD5,這裡我用的加密工具是spark-md5,你可以通過npm install spark-md5 --save來安裝,也可以使用其他MD5加密工具。
  • file有個屬性是uniqueIdentifier,代表檔案唯一標示,我們把計算出來的MD5賦值給這個屬性 file.uniqueIdentifier = md5,這就實現了我們最終的目的。
  • 通過file.resume()開始/繼續檔案上傳。
/**
* 計算md5,實現斷點續傳及秒傳
* @param file
*/
/**
* 計算md5,實現斷點續傳及秒傳
* @param file
*/
 computeMD5(file) {
 let fileReader = new FileReader();
 let time = new Date().getTime();
 let blobSlice = File.prototype.slice || File.prototype.mozSlice || File.prototype.webkitSlice;
 let currentChunk = 0;
 const chunkSize = 10 * 1024 * 1000;
 let chunks = Math.ceil(file.size / chunkSize);
 let spark = new SparkMD5.ArrayBuffer();
 
 // 檔案狀態設為"計算MD5"
 this.statusSet(file.id,'md5');
 
 file.pause();
 
 loadNext();
 
 fileReader.onload = (e => {
 spark.append(e.target.result);
 if (currentChunk < chunks) {
  currentChunk++;
  loadNext();
  // 實時展示MD5的計算進度
  this.$nextTick(() => {
  $(`.myStatus_${file.id}`).text('校驗MD5 '+ ((currentChunk/chunks)*100).toFixed(0)+'%')
  })
 } else {
  let md5 = spark.end();
  this.computeMD5Success(md5,file);
  console.log(`MD5計算完畢:$http://www.cppcns.com{file.name} \nMD5:${md5} \n分片:${chunks} 大小:${file.size} 用時:${new Date().getTime() - time} ms`);
 }
 });
 fileReader.onerror = function () {
 this.error(`檔案${file.name}讀取出錯,請檢查該檔案`)
 file.cancel();
 };
 function loadNext() {
 let start = currentChunk * chunkSize;
 let end = ((start + chunkSize) >= file.size) ? file.size : start + chunkSize;
 fileReader.readAsArrayBuffer(blobSlice.call(file.file,start,end));
 }
},程式設計客棧

computeMD5Success(md5,file) {
 // 將自定義引數直接載入uploader例項的opts上
 Object.assign(this.uploader.opts,{
 query: {
  ...this.params,}
 })
 file.uniqueIdentifier = md5;
 file.resume();
 this.statusRemove(file.id);
},

給file的uniqueIdentifier 屬性賦值後,請求中的identifier即是我們計算出來的MD5

基於vue-simple-uploader封裝檔案分片上傳、秒傳及斷點續傳的全域性上傳外掛功能

7. 秒傳及斷點續傳

在計算完MD5後,我們就能談斷點續傳及秒傳的概念了。

伺服器程式設計客棧根據前端傳過來的MD5去判斷是否可以進行秒傳或斷點續傳:

  • a. 伺服器發現檔案已經完全上傳成功,則直接返回秒傳的標識。
  • b. 伺服器發現檔案上傳過分片資訊,則返回這些分片資訊,告訴前端繼續上傳,即斷點續傳。

7.1 對於前端來說

在每次上傳過程的最開始,vue-simple-uploader會發送一個get請求,來問伺服器我哪些分片已經上傳過了,

基於vue-simple-uploader封裝檔案分片上傳、秒傳及斷點續傳的全域性上傳外掛功能

這個請求返回的結果也有幾種可能:

a. 如果是秒傳,在請求結果中會有相應的標識,比如我這裡是skipUploadtrue,且返回了url,代表伺服器告訴我們這個檔案已經有了,我直接把url給你,你不用再傳了,這就是秒傳。

圖a1:秒傳情況下後臺返回值

基於vue-simple-uploader封裝檔案分片上傳、秒傳及斷點續傳的全域性上傳外掛功能

圖a2:秒傳gif

基於vue-simple-uploader封裝檔案分片上傳、秒傳及斷點續傳的全域性上傳外掛功能

b. 如果後臺返回了分片資訊,這是斷點續傳。如圖,返回的資料中有個uploaded的欄位,代表這些分片是已經上傳過的了,外掛會自動跳過這些分片的上傳。

圖b1:斷點續傳情況下後臺返回值

基於vue-simple-uploader封裝檔案分片上傳、秒傳及斷點續傳的全域性上傳外掛功能

圖b2:斷點續傳gif

基於vue-simple-uploader封裝檔案分片上傳、秒傳及斷點續傳的全域性上傳外掛功能

c. 可能什麼都不會返回,那這就是個全新的檔案了,走完整的分片上傳邏輯

7.2 前端做分片檢驗:checkChunkUploadedByResponse

前面講的是概念,現在說一說前端在拿到這些返回值之後怎麼處理。
外掛自己是不會判斷哪個需要跳過的,在程式碼中由options中的checkChunkUploadedByResponse控制,它會根據 XHR 響應內容檢測每個塊是否上傳成功了,成功的分片直接跳過上傳
你要在這個函式中進行處理,可以跳過的情況下返回true即可。

checkChunkUploadedByResponse: function (chunk,message) {
	 let objMessage = JSON.parse(message);
 if (objMessage.skipUpload) {
  return true;
 }

 return (objMessage.uploaded || []).indexOf(chunk.offset + 1) >= 0
},

注:skipUploaduploaded 是我和後臺商議的欄位,你要按照後臺實際返回的欄位名來。

8. 原始碼及後記

總共幾個檔案,app.vue,封裝的全域性上傳元件globalUploader.vue,呼叫元件的demo.vue,原始碼放到github上了:https://github.com/shady-xia/Blog/tree/master/vue-simple-uploader。

globalUploader原始碼中的ticketapi都是自己用的, 一個是accesstoken,一個是基於axios封裝的請求庫,請根據你的業務需求替代之。另外上傳介面的展開和收起用到了jquery,通知用到了Element的元件,請忽略之。

本人水平有限,更多的是提供一個思路,供大家參考。

封裝完這個外掛後,再加上開發檔案資源庫,我發現已經基本實現了一個簡易的百度網盤了,一個管理系統,功能搞的這麼複雜,坑爹啊!

8.1 關於第一個分片丟失問題

關於開啟了testChunk後伺服器收不到第一個分片的問題:
simpleUploader文件上是這麼寫的:

基於vue-simple-uploader封裝檔案分片上傳、秒傳及斷點續傳的全域性上傳外掛功能

testChunk的那個get請求,預設帶了第一個分片給服務端,如果服務端返回的是200狀態,則假定當前塊已經上傳過了,不會再上傳了;
所以這裡伺服器要改成其他http狀態碼,比如204,這樣就不在“ 200,201,202”這個集合裡了,代表服務端還沒有這個塊,需要按照標準模式上傳,這樣第一個分片就會再次被上傳了

2019/8/6更新

1、優化了計算檔案MD5的方式,展示MD5的計算進度

之前文章中計算MD5的方式為對整個檔案直接計算MD5,很吃記憶體,容易導致瀏覽器崩潰
我改成了通過分片讀取檔案的方式計算MD5,防止直接讀取大檔案時因記憶體佔用過大導致的網頁卡頓、崩潰

2、新增了的自定義的狀態

(之前我就封裝了幾種自定義狀態,最近總有小夥伴問怎麼沒有“校驗MD5”,“合併中”這些狀態,我就把我的方法寫出來了,方式很笨,但是能實現效果)

外掛原本只支援了successerroruploadingpausedwaiting這幾種狀態,

由於業務需求,我額外增加了“校驗MD5”“合併中”“轉碼中”“上傳失敗”這幾種自定義的狀態

由於前幾種狀態是外掛已經封裝好的,我不能改原始碼,只能用比較hack的方式:
當自定義狀態開始時,要手動調一下statusSet方法,生成一個p標籤蓋在原本的狀態上面;當自定義狀態結束時,還要手動呼叫statusRemove移除該標籤。

this.statusSet(file.id,'merging');
this.statusRemove(file.id);

具體使用可以參考原始碼,同時希望simple-uploader的外掛作者後面能夠支援自定義狀態的配置。

到此這篇關於基於vue-simple-uploader封裝檔案分片上傳、秒傳及斷點續傳的全域性上傳外掛功能的文章就介紹到這了,更多相關vue simple uploader封裝內容請搜尋我們以前的文章或繼續瀏覽下面的相關文章希望大家以後多多支援我們!