基於vue-simple-uploader封裝文件分片上傳、秒傳及斷點續傳的全局上傳插件

1. 前言

以前公司要在管理系統中作一個全局上傳插件,即切換各個頁面的時候,上傳界面還在而且上傳不會受到影響,這在vue這種spa框架面前並非什麼難題。然然後端大佬說咱們要實現分片上傳秒傳以及斷點續傳的功能,聽起來頭都大了。html

好久以前我寫了一篇webuploader的文章,結果使用起來發現問題不少,且官方團隊再也不維護這個插件了, 通過多天調研及踩雷,最終決定基於vue-simple-uploader插件實現該功能,在項目中使用起來無痛且穩定。前端

若是你只是想實現基本的(非定製化的)上傳功能,直接使用vue-simple-uploader,多讀一下它的文檔,不須要更多的二次封裝。
若是你只是想實現全局上傳插件,也能夠參照一下個人實現。
若是你用到了分片上傳、秒傳及斷點續傳這些複雜的功能,恭喜你,這篇文章的重點就在於此。vue

本文源碼在此:https://github.com/shady-xia/Blog/tree/master/vue-simple-uploaderjava

2. 關於vue-simple-uploader

vue-simple-uploader是基於 simple-uploader.js 封裝的vue上傳插件。它的優勢包括且不限於如下幾種:jquery

  • 支持文件、多文件、文件夾上傳;支持拖拽文件、文件夾上傳
  • 可暫停、繼續上傳
  • 錯誤處理
  • 支持「秒傳」,經過文件判斷服務端是否已存在從而實現「秒傳」
  • 分塊上傳
  • 支持進度、預估剩餘時間、出錯自動重試、重傳等操做

讀這篇文章以前,建議先讀一遍simple-uploader.js的文檔,而後再讀一下vue-simple-uploader的文檔,瞭解一下各個參數的做用是什麼,我在這裏假定你們已經比較熟悉了。。
vue-simple-uploader文檔ios

simple-uploader.js文檔git

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

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_CONFIG.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, file, 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, file, response, chunk) { console.log(error) },

5. 文件分片

vue-simple-uploader自動將文件進行分片,在optionschunkSize中能夠設置每一個分片的大小。

如圖:對於大文件來講,會發送多個請求,在設置testChunkstrue後(在插件中默認就是true),會發送與服務器進行分片校驗的請求,下面的第一個get請求就是該請求;後面的每個post請求都是上傳分片的請求

看一下發送給服務端的參數,其中chunkNumber表示當前是第幾個分片,totalChunks表明全部的分片數,這兩個參數都是都是插件根據你設置的chunkSize來計算的。

須要注意的就是在最後文件上傳成功的事件中,經過後臺返回的字段,來判斷是否要再給後臺發送一個文件合併的請求。

6. MD5的計算過程

斷點續傳及秒傳的基礎是要計算文件的MD5,這是文件的惟一標識,而後服務器根據MD5進行判斷,是進行秒傳仍是斷點續傳。

file-added事件以後,就計算MD5,咱們最終的目的是將計算出來的MD5加到參數裏傳給後臺,而後繼續文件上傳的操做,詳細的思路步驟是:

  1. 把uploader組件的autoStart設爲false,即選擇文件後不會自動開始上傳
  2. 先經過 file.pause()暫停文件,而後經過H5的FileReader接口讀取文件
  3. 將異步讀取文件的結果進行MD5,這裏我用的加密工具是spark-md5,你能夠經過npm install spark-md5 --save來安裝,也可使用其餘MD5加密工具。
  4. file有個屬性是uniqueIdentifier,表明文件惟一標示,咱們把計算出來的MD5賦值給這個屬性 file.uniqueIdentifier = md5,這就實現了咱們最終的目的。
  5. 經過file.resume()開始/繼續文件上傳。
/** * 計算md5,實現斷點續傳及秒傳 * @param file */ computeMD5(file) { let fileReader = new FileReader(); let time = new Date().getTime(); let md5 = ''; file.pause(); fileReader.readAsArrayBuffer(file.file); fileReader.onload = (e => { if (file.size != e.target.result.byteLength) { this.error('Browser reported success but could not read the file until the end.'); return } md5 = SparkMD5.ArrayBuffer.hash(e.target.result); // 添加額外的參數 this.uploader.opts.query = { ...this.params } console.log(`MD5計算完畢:${file.id} ${file.name} MD5:${md5} 用時:${new Date().getTime() - time} ms`); file.uniqueIdentifier = md5; file.resume(); }); fileReader.onerror = function () { this.error('FileReader onerror was triggered, maybe the browser aborted due to high memory usage.'); }; },

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

7. 秒傳及斷點續傳

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

服務器根據前端傳過來的MD5去判斷是否能夠進行秒傳或斷點續傳:

  • a. 服務器發現文件已經徹底上傳成功,則直接返回秒傳的標識。
  • b. 服務器發現文件上傳過度片信息,則返回這些分片信息,告訴前端繼續上傳,即斷點續傳

7.1 對於前端來講

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

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

  • a. 若是是秒傳,在請求結果中會有相應的標識,好比我這裏是skipUploadtrue,且返回了url,表明服務器告訴咱們這個文件已經有了,我直接把url給你,你不用再傳了,這就是秒傳

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

圖a2:秒傳gif

  • b. 若是後臺返回了分片信息,這是斷點續傳。如圖,返回的數據中有個uploaded的字段,表明這些分片是已經上傳過的了,插件會自動跳過這些分片的上傳。

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

圖b2:斷點續傳gif

  • 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 },

注:skipUpload 和 uploaded 是我和後臺商議的字段,你要按照後臺實際返回的字段名來。

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的組件,請忽略之。

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

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

相關文章
相關標籤/搜索