基于vue-simple-uploader封裝文件分片上傳、秒傳及斷點續(xù)傳的全局上傳插件功能
1. 前言
之前公司要在管理系統(tǒng)中做一個全局上傳插件,即切換各個頁面的時候,上傳界面還在并且上傳不會受到影響,這在vue這種spa框架面前并不是什么難題。然而后端大佬說我們要實現(xiàn)分片上傳、秒傳以及斷點續(xù)傳的功能,聽起來頭都大了。
很久之前我寫了一篇webuploader的文章,結果使用起來發(fā)現(xiàn)問題很多,且官方團隊不再維護這個插件了, 經(jīng)過多天調(diào)研及踩雷,最終決定基于vue-simple-uploader
插件實現(xiàn)該功能,在項目中使用起來無痛且穩(wěn)定。
如果你只是想實現(xiàn)基本的(非定制化的)上傳功能,直接使用vue-simple-uploader
,多讀一下它的文檔,不需要更多的二次封裝。
如果你只是想實現(xiàn)全局上傳插件,也可以參照一下我的實現(xiàn)。
如果你用到了分片上傳、秒傳及斷點續(xù)傳這些復雜的功能,恭喜你,這篇文章的重點就在于此。
本文源碼在此:https://github.com/shady-xia/Blog/tree/master/vue-simple-uploader
2. 關于vue-simple-uploader
vue-simple-uploader
是基于 simple-uploader.js
封裝的vue上傳插件。它的優(yōu)點包括且不限于以下幾種:
- 支持文件、多文件、文件夾上傳;支持拖拽文件、文件夾上傳
- 可暫停、繼續(xù)上傳
- 錯誤處理
- 支持“秒傳”,通過文件判斷服務端是否已存在從而實現(xiàn)“秒傳”
- 分塊上傳
- 支持進度、預估剩余時間、出錯自動重試、重傳等操作
讀這篇文章之前,建議先讀一遍simple-uploader.js
的文檔,然后再讀一下vue-simple-uploader
的文檔,了解一下各個參數(shù)的作用是什么,我在這里假定大家已經(jīng)比較熟悉了。。
vue-simple-uploader文檔
安裝: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部分暫時不列出,大家可以根據(jù)自己的ui去更改,主要關注一下uploader
這個組件的options
參數(shù)及文件added
、success
、progress
、error
幾個事件:
<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', //上傳文件時文件的參數(shù)名,默認file maxChunkRetries: 3, //最大自動失敗重試上傳次數(shù) testChunks: true, //是否開啟服務器分片校驗 // 服務器分片校驗函數(shù),秒傳及斷點續(xù)傳基礎 checkChunkUploadedByResponse: function (chunk, message) { let objMessage = JSON.parse(message); if (objMessage.skipUpload) { return true; } return (objMessage.uploaded || []).indexOf(chunk.offset + 1) >= 0 }, headers: { // 在header中添加的驗證,請根據(jù)實際業(yè)務來 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. 點擊按鈕,觸發(fā)文件上傳操作:
(如果你做的不是全局上傳的功能,而是直接點擊上傳,忽略這一步。)
因為我做的是全局上傳的插件,要先把上傳的窗口隱藏起來,在點擊某個上傳按鈕的時候,用Bus發(fā)送一個openUploader
的事件,在globalUploader.vue
中接收該事件,trigger我們uploader-btn
的click事件。
在某個頁面中,點擊上傳按鈕,同時把要給后臺的參數(shù)帶過來(如果有的話),這里組件之間傳值我用的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,以此來實現(xiàn)斷點續(xù)傳及秒傳的功能,所以選擇文件后直接開始上傳肯定不行,要等MD5計算完畢之后,再開始文件上傳的操作。
具體的MD5計算方法,會在下面講,這里先簡單引出。
上傳過程中,會不斷觸發(fā)file-progress上傳進度的回調(diào)
// 文件進度的回調(diào) onFileProgress(rootFile, file, chunk) { console.log(`上傳中 ${file.name},chunk:${chunk.startByte / 1024 / 1024} ~ ${chunk.endByte / 1024 / 1024}`) },
3. 文件上傳成功后
文件上傳成功后,在“上傳完成”的回調(diào)中,通過服務端返回的needMerge
字段,來判斷是否需要再發(fā)送合并分片的請求,
如果這個字段為true,則需要給后臺發(fā)一個請求合并的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
自動將文件進行分片,在options
的chunkSize
中可以設置每個分片的大小。
如圖:對于大文件來說,會發(fā)送多個請求,在設置testChunks
為true
后(在插件中默認就是true
),會發(fā)送與服務器進行分片校驗的請求,下面的第一個get請求就是該請求;后面的每一個post請求都是上傳分片的請求
看一下發(fā)送給服務端的參數(shù),其中chunkNumber
表示當前是第幾個分片,totalChunks
代表所有的分片數(shù),這兩個參數(shù)都是都是插件根據(jù)你設置的chunkSize
來計算的。
需要注意的就是在最后文件上傳成功的事件中,通過后臺返回的字段,來判斷是否要再給后臺發(fā)送一個文件合并的請求。
6. MD5的計算過程
斷點續(xù)傳及秒傳的基礎是要計算文件的MD5
,這是文件的唯一標識,然后服務器根據(jù)MD5
進行判斷,是進行秒傳還是斷點續(xù)傳。
在file-added
事件之后,就計算MD5
,我們最終的目的是將計算出來的MD5
加到參數(shù)里傳給后臺,然后繼續(xù)文件上傳的操作,詳細的思路步驟是:
- 把uploader組件的
autoStart
設為false
,即選擇文件后不會自動開始上傳 - 先通過
file.pause()
暫停文件,然后通過H5的FileReader
接口讀取文件 - 將異步讀取文件的結果進行
MD5
,這里我用的加密工具是spark-md5
,你可以通過npm install spark-md5 --save
來安裝,也可以使用其他MD5加密工具。 - file有個屬性是
uniqueIdentifier
,代表文件唯一標示,我們把計算出來的MD5賦值給這個屬性file.uniqueIdentifier = md5
,這就實現(xiàn)了我們最終的目的。 - 通過
file.resume()
開始/繼續(xù)文件上傳。
/** * 計算md5,實現(xiàn)斷點續(xù)傳及秒傳 * @param file */ /** * 計算md5,實現(xiàn)斷點續(xù)傳及秒傳 * @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(); // 文件狀態(tài)設為"計算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計算完畢:${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) { // 將自定義參數(shù)直接加載uploader實例的opts上 Object.assign(this.uploader.opts, { query: { ...this.params, } }) file.uniqueIdentifier = md5; file.resume(); this.statusRemove(file.id); },
給file的uniqueIdentifier 屬性賦值后,請求中的identifier即是我們計算出來的MD5
7. 秒傳及斷點續(xù)傳
在計算完MD5
后,我們就能談斷點續(xù)傳及秒傳的概念了。
服務器根據(jù)前端傳過來的MD5
去判斷是否可以進行秒傳或斷點續(xù)傳:
- a. 服務器發(fā)現(xiàn)文件已經(jīng)完全上傳成功,則直接返回秒傳的標識。
- b. 服務器發(fā)現(xiàn)文件上傳過分片信息,則返回這些分片信息,告訴前端繼續(xù)上傳,即斷點續(xù)傳。
7.1 對于前端來說
在每次上傳過程的最開始,vue-simple-uploader
會發(fā)送一個get請求,來問服務器我哪些分片已經(jīng)上傳過了,
這個請求返回的結果也有幾種可能:
a. 如果是秒傳,在請求結果中會有相應的標識,比如我這里是skipUpload
為true
,且返回了url
,代表服務器告訴我們這個文件已經(jīng)有了,我直接把url
給你,你不用再傳了,這就是秒傳。
圖a1:秒傳情況下后臺返回值
圖a2:秒傳gif
b. 如果后臺返回了分片信息,這是斷點續(xù)傳。如圖,返回的數(shù)據(jù)中有個uploaded
的字段,代表這些分片是已經(jīng)上傳過的了,插件會自動跳過這些分片的上傳。
圖b1:斷點續(xù)傳情況下后臺返回值
圖b2:斷點續(xù)傳gif
c. 可能什么都不會返回,那這就是個全新的文件了,走完整的分片上傳邏輯
7.2 前端做分片檢驗:checkChunkUploadedByResponse
前面講的是概念,現(xiàn)在說一說前端在拿到這些返回值之后怎么處理。
插件自己是不會判斷哪個需要跳過的,在代碼中由options
中的checkChunkUploadedByResponse
控制,它會根據(jù) XHR 響應內(nèi)容檢測每個塊是否上傳成功了,成功的分片直接跳過上傳
你要在這個函數(shù)中進行處理,可以跳過的情況下返回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
,調(diào)用組件的demo.vue
,源碼放到github上了:https://github.com/shady-xia/Blog/tree/master/vue-simple-uploader。
globalUploader
源碼中的ticket
和api
都是自己用的, 一個是accesstoken,一個是基于axios封裝的請求庫,請根據(jù)你的業(yè)務需求替代之。另外上傳界面的展開和收起用到了jquery
,通知用到了Element的組件,請忽略之。
本人水平有限,更多的是提供一個思路,供大家參考。
封裝完這個插件后,再加上開發(fā)文件資源庫,我發(fā)現(xiàn)已經(jīng)基本實現(xiàn)了一個簡易的百度網(wǎng)盤了,一個管理系統(tǒng),功能搞的這么復雜,坑爹啊!
8.1 關于第一個分片丟失問題
關于開啟了testChunk后服務器收不到第一個分片的問題:
simpleUploader文檔上是這么寫的:
testChunk的那個get請求,默認帶了第一個分片給服務端,如果服務端返回的是200狀態(tài),則假定當前塊已經(jīng)上傳過了,不會再上傳了;
所以這里服務器要改成其他http狀態(tài)碼,比如204,這樣就不在“ 200, 201, 202”這個集合里了,代表服務端還沒有這個塊,需要按照標準模式上傳,這樣第一個分片就會再次被上傳了
2019/8/6更新
1、優(yōu)化了計算文件MD5的方式,展示MD5的計算進度
之前文章中計算MD5
的方式為對整個文件直接計算MD5
,很吃內(nèi)存,容易導致瀏覽器崩潰
我改成了通過分片讀取文件的方式計算MD5
,防止直接讀取大文件時因內(nèi)存占用過大導致的網(wǎng)頁卡頓、崩潰
2、新增了的自定義的狀態(tài)
(之前我就封裝了幾種自定義狀態(tài),最近總有小伙伴問怎么沒有“校驗MD5”,“合并中”這些狀態(tài),我就把我的方法寫出來了,方式很笨,但是能實現(xiàn)效果)
插件原本只支持了success
、error
、uploading
、paused
、waiting
這幾種狀態(tài),
由于業(yè)務需求,我額外增加了“校驗MD5”
、“合并中”
、“轉碼中”
、“上傳失敗”
這幾種自定義的狀態(tài)
由于前幾種狀態(tài)是插件已經(jīng)封裝好的,我不能改源碼,只能用比較hack的方式:
當自定義狀態(tài)開始時,要手動調(diào)一下statusSet
方法,生成一個p
標簽蓋在原本的狀態(tài)上面;當自定義狀態(tài)結束時,還要手動調(diào)用statusRemove
移除該標簽。
this.statusSet(file.id, 'merging'); this.statusRemove(file.id);
具體使用可以參考源碼,同時希望simple-uploader的插件作者后面能夠支持自定義狀態(tài)的配置。
到此這篇關于基于vue-simple-uploader封裝文件分片上傳、秒傳及斷點續(xù)傳的全局上傳插件功能的文章就介紹到這了,更多相關vue simple uploader封裝內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關文章希望大家以后多多支持腳本之家!
- 利用Vue3+Element-plus實現(xiàn)大文件分片上傳組件
- 使用Vue3+ElementPlus前端實現(xiàn)分片上傳的全過程
- vue+element+oss實現(xiàn)前端分片上傳和斷點續(xù)傳
- springboot整合vue2-uploader實現(xiàn)文件分片上傳、秒傳、斷點續(xù)傳功能
- vue大文件分片上傳之simple-uploader.js的使用
- vue?大文件分片上傳(斷點續(xù)傳、并發(fā)上傳、秒傳)
- vue實現(xiàn)大文件分片上傳與斷點續(xù)傳到七牛云
- Vue2.0結合webuploader實現(xiàn)文件分片上傳功能
- vue3+element 分片上傳與分片下載功能實現(xiàn)方法詳解
相關文章
vue項目持久化存儲數(shù)據(jù)的實現(xiàn)代碼
這篇文章主要介紹了vue項目持久化存儲數(shù)據(jù)的實現(xiàn)代碼,小編覺得挺不錯的,現(xiàn)在分享給大家,也給大家做個參考。一起跟隨小編過來看看吧2018-10-10vue結合el-table實現(xiàn)表格行拖拽排序(基于sortablejs)
這篇文章主要介紹了vue結合el-table實現(xiàn)表格行拖拽排序(基于sortablejs),本文通過實例代碼給大家介紹的非常詳細,對大家的學習或工作具有一定的參考借鑒價值,需要的朋友可以參考下2024-01-01Vue實現(xiàn)讓頁面加載時請求后臺接口數(shù)據(jù)
這篇文章主要介紹了Vue實現(xiàn)讓頁面加載時請求后臺接口數(shù)據(jù)2022-08-08