基于element-ui自定義封裝大文件上傳組件的案例分享
以element-ui基礎(chǔ)封裝大文件上傳的組件,包括斷點續(xù)傳,秒傳,上傳進度條,封裝思想邏輯來源于el-upload 組件源碼,看下面具體案例
<div class="big-file-box"> <div @click="handleClick"> <slot></slot> <input class="el-upload__input" type="file" ref="input" name="請上傳文件" @change='handleChange' :multiple='multiple' :accept='accept'></input> </div> <slot name="tip"></slot> <div class="file-box"> <transition-group tag="ul"> <li v-for="item in uploadFiles" :key="item.uid" tabindex="0" :class="{'file-li':true}" @mouseover="handleMouseover(item.uid)" @mouseleave="handleMouseleave(item.uid)" > <i class="el-icon-document"></i> <span class="file-name" @click="handleClickFile(item)">{{item.name || item.url}}</span> <i v-if="item.status === 'success'" :class="item.uid === listChecked ? 'el-icon-close deleteColor': 'el-icon-circle-check passColor'" @click="hanldeRemoveFile(item)"></i> <el-progress v-if="item.status === 'uploading'" type="line" :stroke-width="2" :percentage="Number(item.percentage.toFixed(1))"> </el-progress> </li> </transition-group> </div> </div>
看上面代碼,主要分為3部分:
- 1、文件按鈕部分,一個默認(rèn)插槽加一個input框,默認(rèn)插槽用來自定義上傳框的樣式,input大家都懂就是原生的上傳框,注意這個input 是需要隱藏的,這里偷懶直接用了element的類名
- 2、上傳文件類型提示部分,一個文件類型提示的具名插槽 name="tip",用來自定義樣式給出提示的文案
- 3、已上傳的文件列表,用來點擊預(yù)覽,刪除,以及上傳進度條的展示,進度條部分會有status ,是文件上傳的狀態(tài),當(dāng)為uploading 時渲染
接下來是js 部分,分片部分的邏輯就不在這篇文章里面贅述了。
首先看組件的props
prop | 類型 | 描述 |
---|---|---|
beforeUpload | Function(file) | 文件上傳前鉤子上傳文件之前的鉤子,參數(shù)為上傳的文件,若返回 false 則停止上傳 |
onExceed | Function(file,fileList) | 文件超出個數(shù)限制時的鉤子 |
limit | Number | 文件限制數(shù)量 |
uploadApi | String | 上傳文件的接口 |
mergeApi | String | 文件上傳成功后,合并文件接口 |
checkApi | String | 檢測文件上傳分片接口,返回已上傳所有片段的索引 |
accept | String | 允許上傳的文件類型 |
concurrentUpload | Boolean | 是否允許并發(fā)請求(后端服務(wù)帶寬受限,可能需要同步依次上傳分片,而不是瞬間發(fā)起幾百個請求) |
fileList | Array | 上傳的文件列表, 例如: [{name: 'food.jpg', url: 'xxx.cdn.com/xxx.jpg'}] |
onRemove | Function(file,fileList) | 文件列表移除文件時的鉤子 |
onChange | Function(file,fileList) | 文件狀態(tài)改變時的鉤子,添加文件時調(diào)用 |
onPreview | Function(file) | 文件預(yù)覽鉤子 |
onSuccess | Function(file,url) | 文件合并成功鉤子,返回成功文件,和后端存儲到S3的url |
onError | Function(file) | 文件上傳失敗鉤子 |
onProgress | Function(file,percentage) | 文件上傳進度鉤子 |
onReadingFile | Function(status) | 讀取文件md5時的鉤子函數(shù),參數(shù) start:開始讀取,end:讀取結(jié)束 |
chunckSize | Number | 文件切片大小 |
request | Function | 封裝好的axios,用于請求的工具函數(shù) |
apiHeader | Object | 需要的特殊請求頭 |
SparkMD5 | Function | 讀取文件md5的工具函數(shù)(spark-md5直接安裝這個包) |
multiple | Boolean | 是否可多選文件(建議不多選,大文件有瓶頸) |
ok 開始上傳,下面我們一步步來解析
第一步選取文件
handleClick() { this.$refs.input.value = null; this.$refs.input.click(); },
重置上一次的文件,接著主動觸發(fā)input 的click事件
async handleChange(ev){ const files = ev.target.files; if (!files) return; this.uploadExceedFiles(files); }, uploadExceedFiles(files) { if (this.limit && this.fileList.length + files.length > this.limit) { //大于限制數(shù)量,父組件處理自己的邏輯 this.onExceed && this.onExceed(files, this.fileList); return; } this.upload(files) }, async upload(files){ if (!this.beforeUpload) { this.readFile(files[0]); } const before = this.beforeUpload(files[0]); if(before) { this.readFile(files[0]) } },
觸發(fā)input的change事件,開始判斷是否已選取文件,接著判斷文件個數(shù),如果超出限制,會直接終止當(dāng)前邏輯并將文件,以及文件列表拋給父組件的onExceed 函數(shù),父組件自行給出提示,如果未超過限制,繼續(xù)執(zhí)行上傳邏輯執(zhí)行 upload 方法, upload 方法會調(diào)用beforeUpload 方法是否符合文件類型,如果返回ture, 繼續(xù)執(zhí)行,開始讀取大文件的md5(這里是關(guān)鍵)
繼續(xù)看readFile方法:
async readFile(files) { this.sliceFile(files); //注意這里,開始讀取文件,會回調(diào)父組件的onreadingFile,告訴組件開始讀取,此時父組件開始設(shè)置讀取的loading 狀態(tài),讀取完成之后再次調(diào)用會返回end表示讀取結(jié)束,此時將loading狀態(tài)改為false this.onReadingFile('start'); const data = await this.getFileMD5(files); this.onReadingFile('end'); //判斷是否上傳重復(fù)文件 const hasSameFile = this.uploadFiles.findIndex(item=> item.hash ===data); if(hasSameFile === -1) { this.fileSparkMD5 = {md5Value:data,fileKey:files.name}; const hasChuncks = await this.checkFile(data,files.name); //是否上傳過 let isSuccess = true; //同步上傳成功標(biāo)記 //斷點續(xù)傳 if(hasChuncks) { const hasEmptyChunk = this.isUploadChuncks.findIndex(item => item === 0); //上傳過,并且已經(jīng)完整上傳,直接提示上傳成功(秒傳) if(hasEmptyChunk === -1) { let file = { status: 'success', percentage: 100, uid: Date.now() + this.tempIndex++, hash:data, name:'', url:'' }; this.uploadFiles.push(file); this.onSuccess(file); return; } else { //處理續(xù)傳邏輯,上傳檢測之后未上傳的分片 this.onStart(files,data); const emptyLength = this.isUploadChuncks.filter(item => item === 0); for(let k = 0; k < this.isUploadChuncks.length; k++) { if(this.isUploadChuncks[k] !== 1) { let formData = new FormData(); formData.append('totalNumber',this.fileChuncks.length); formData.append("chunkSize",this.chunckSize); formData.append("partNumber",k); formData.append('uuid',this.fileSparkMD5.md5Value); formData.append('name',this.fileSparkMD5.fileKey); formData.append('file',new File([this.fileChuncks[k].fileChuncks],this.fileSparkMD5.fileKey)) //如果并發(fā)請求,走這里 if(this.concurrentUpload) { this.post(formData,k,emptyLength.length,data); }else { isSuccess = await this.post(formData,k,emptyLength.length,data);//這注意分片總數(shù),因為進度條是根據(jù)分片個數(shù)來算的,所以分母應(yīng)該是未上傳的分片總數(shù) if(!isSuccess) { break; } } } } //兼容并發(fā)與同步請求操作,受服務(wù)器帶寬影響,做并發(fā)與同步請求處理 if(this.concurrentUpload) { this.uploadSuccess(); }else { if(isSuccess) { //執(zhí)行玩循環(huán),如果isSuccess還是true,說明所有分片已上傳,可執(zhí)行合并文件接口 this.mergeFile(this.fileSparkMD5,this.fileChuncks.length); }else { const index = this.uploadFiles.findIndex(item => item.hash === this.fileSparkMD5.md5Value); this.$set(this.uploadFiles,index,{...this.uploadFiles[index],...{status: 'error'}}); //后續(xù)拓展繼續(xù)上傳時可用 this.hanldeRemoveFile(this.uploadFiles[index]); this.onError(this.uploadFiles[index]); } } } }else { this.onStart(files,data); // this.sliceFile(files); //同步上傳 for(let i = 0; i < this.fileChuncks.length; i++) { let formData = new FormData(); formData.append('totalNumber',this.fileChuncks.length); formData.append("chunkSize",this.chunckSize); formData.append("partNumber",i); formData.append('uuid',this.fileSparkMD5.md5Value); formData.append('name',this.fileSparkMD5.fileKey); formData.append('file',new File([this.fileChuncks[i].fileChuncks],this.fileSparkMD5.fileKey)); if(this.concurrentUpload) { this.post(formData,k,emptyLength.length,data); }else { isSuccess = await this.post(formData,i,this.fileChuncks.length,data);//這注意分片總數(shù),因為進度條是根據(jù)分片個數(shù)來算的,所以分母應(yīng)該是未上傳的分片總數(shù) if(!isSuccess) { break; } } } //兼容并發(fā)與同步請求操作,受服務(wù)器帶寬影響,做并發(fā)與同步請求處理 if(this.concurrentUpload) { this.uploadSuccess(); }else { //循環(huán)所有的片段后,isSuccess依然為ture if(isSuccess) { this.mergeFile(this.fileSparkMD5,this.fileChuncks.length); }else { const index = this.uploadFiles.findIndex(item => item.hash === this.fileSparkMD5.md5Value); this.$set(this.uploadFiles,index,{...this.uploadFiles[index],...{status: 'error'}}); //后續(xù)拓展繼續(xù)上傳時可用 this.hanldeRemoveFile(this.uploadFiles[index]); this.onError(this.uploadFiles[index]); } } } }else { this.$message.error('Please do not upload the same file repeatedly'); } }, onStart(rawFile,hash) { rawFile.uid = Date.now() + this.tempIndex++; let file = { status: 'ready', name: rawFile.name, size: rawFile.size, percentage: 0, uid: rawFile.uid, raw: rawFile, hash }; this.uploadFiles.push(file); this.onChange(file, this.uploadFiles); }, sliceFile (file) { //文件分片之后的集合 const chuncks = []; let start = 0 ; let end; while(start < file.size) { end = Math.min(start + this.chunckSize,file.size); chuncks.push({fileChuncks:file.slice(start,end),fileName:file.name}); start = end; } this.fileChuncks = [...chuncks]; }, getFileMD5 (file){ return new Promise((resolve, reject) => { const fileReader = new FileReader(); fileReader.onload = (e) =>{ const fileMd5 = this.SparkMD5.ArrayBuffer.hash(e.target.result); resolve(fileMd5) } fileReader.onerror = (e) =>{ reject('file read failure',e) this.onError(file,'file read failure') } fileReader.readAsArrayBuffer(file); }) }, async checkFile(md5Hash,fileName) { const {code,data} = await this.request({url:`${this.checkApi}?uuid=${md5Hash}&fileName=${fileName}`, method: 'get'}); if(code === 200) { if(data.length) { const newArr = new Array(Number(this.fileChuncks.length)).fill(0); // [1,1,0,1,1] const chunckNumberArr = data.map(item => item); chunckNumberArr.forEach((item,index) => { newArr[item] = 1 }); this.isUploadChuncks = [...newArr]; return true; }else { return false; } } } //并發(fā)請求,推入promise 數(shù)組中,通過allSettled 方法來判斷,所有任務(wù)是否都為resove狀態(tài),如果有是,就進行合并文件 uploadSuccess() { Promise.allSettled(this.promiseArr).then(result=>{ const hasReject = result.findIndex(item => item.status === 'rejected'); if(hasReject === -1) { this.mergeFile(this.fileSparkMD5,this.fileChuncks.length); }else { const index = this.uploadFiles.findIndex(item => item.hash === result[hasReject].reason); this.$set(this.uploadFiles,index,{...this.uploadFiles[index],...{status: 'error'}}); //后續(xù)拓展繼續(xù)上傳時可用 this.hanldeRemoveFile(this.uploadFiles[index]); this.onError(this.uploadFiles[index]); } }).catch(e=>{ this.onError(e); }).finally(e=>{ this.promiseArr = []; this.uploadQuantity = 0; //重置上傳進度 }) },
首先將文件開始切片,放入切片list中,接著開始讀取文件,這里可以自行在父組件中調(diào)用 onReadingFile 方法設(shè)置loading狀態(tài),提升用戶體驗度。 接著會直接調(diào)用服務(wù)單接口,檢查是否已經(jīng)上傳過,并將已上傳的分片序號寫入到一個isUploadChuncks list中,然后循環(huán)上傳未上片段,這里會執(zhí)行 onStart 方法,給每個文件一個初始對象,設(shè)置文件的初始狀態(tài),以及文件內(nèi)容,插入到已上傳的文件列表 uploadFiles 中,為后根據(jù)文件狀態(tài)展示進度條,以及上傳失敗時刪除對應(yīng)文件列表做準(zhǔn)備
劃重點:調(diào)用接口,處理上傳邏輯,這里主要分兩種。前面提到過,服務(wù)端會有上傳帶寬的限制,如果一次性發(fā)送很多的文件請求,服務(wù)端是接受不了的。所以分2種,并發(fā)上傳,和同步上傳。post 方法會返回一個promise,并生成了一個以每個promise請求,組成的promise 集合
//file:當(dāng)前文件,nowChunck:當(dāng)前分片索引,totalChunck:當(dāng)前需要上傳文件分片的總數(shù),hash:文件的唯一hash值 async post(file,nowChunck,totalChunck,hash) { let _this = this; const index = _this.uploadFiles.findIndex(item => item.hash === hash); _this.$set(_this.uploadFiles,index,{..._this.uploadFiles[index],...{status: 'uploading'}}); const curPormise = new Promise((resolve,reject)=>{ let xhr = new XMLHttpRequest(); // 當(dāng)上傳完成時調(diào)用 xhr.onload = function() { if (xhr.status === 200) { const index = _this.uploadFiles.findIndex(item => item.hash === hash); //大文件上傳進度 _this.uploadQuantity ++; _this.$set(_this.uploadFiles,index,{..._this.uploadFiles[index],...{status: 'uploading',percentage:_this.uploadQuantity / totalChunck * 100}}); _this.onProgress(file,_this.uploadQuantity / totalChunck * 100); resolve(true); }else { _this.errorChuncks.push({file:file,nowChunck,totalChunck,hash}); reject(false); _this.uploadQuantity = 0; const index = _this.uploadFiles.findIndex(item => item.hash === hash); _this.$set(_this.uploadFiles,index,{..._this.uploadFiles[index],...{status: 'error'}}); //后續(xù)拓展繼續(xù)上傳時可用 _this.hanldeRemoveFile(_this.uploadFiles[index]); _this.onError(_this.uploadFiles[index]); } } xhr.onerror = function(e) { _this.errorChuncks.push({file:file,nowChunck,totalChunck,hash}); reject(false) _this.uploadQuantity = 0; const index = _this.uploadFiles.findIndex(item => item.hash === hash); _this.$set(_this.uploadFiles,index,{..._this.uploadFiles[index],...{status: 'error'}}); //后續(xù)拓展繼續(xù)上傳時可用 _this.hanldeRemoveFile(_this.uploadFiles[index]); _this.onError(_this.uploadFiles[index]); } // 發(fā)送請求 xhr.open('POST', _this.uploadApi, true); if (_this.apiHeader?.withCredentials && 'withCredentials' in xhr) { xhr.withCredentials = true; } const headers = _this.apiHeader || {}; for (let item in headers) { if (headers.hasOwnProperty(item) && headers[item] !== null) { xhr.setRequestHeader(item, headers[item]); } } xhr.send(file); }); _this.promiseArr.push(curPormise); return curPormise; },
通過父組件傳遞的concurrentUpload參數(shù),決定是并發(fā)還是同步
uploadSuccess 為并發(fā)時邏輯,將所有的請求放入promise數(shù)組中,如果都成功進行合并文件
這里為同步,因為上面pormise如果成功resove(true),所以成功才會繼續(xù)走遞歸發(fā)送請求,否者立馬中斷上傳
最后就是合并文件,合并之后根據(jù)文件的MD5匹配,然后修改對應(yīng)文件的status,通過狀態(tài)隱藏進度條,這里成功之后會走onSuccess方法,這時可以在父組件放開上傳按鈕禁用的狀態(tài)(看前面的邏輯,會在選擇文件之后,禁用上傳按鈕)
async mergeFile (fileInfo,chunckTotal){ const { md5Value,fileKey } = fileInfo; const params = { totalNumber:chunckTotal, md5:md5Value, name:fileKey } const index = this.uploadFiles.findIndex(item => item.hash === md5Value); try { const {code,data} = await this.request({url:`${this.mergeApi}?uuid=${md5Value}`, method: 'get'}); if(code === 200) { this.$set(this.uploadFiles,index,{...this.uploadFiles[index],...{status: 'success',url:data}}); //記得綁定url this.onSuccess(this.uploadFiles[index],data); } }catch(e) { this.$set(this.uploadFiles,index,{...this.uploadFiles[index],...{status: 'error',url:''}}); //記得綁定url this.hanldeRemoveFile(this.uploadFiles[index]); this.onError(this.uploadFiles[index]); } this.uploadQuantity = 0; this.errorChuncks = []; },
最后看下在父組件中使用的案例
<BigUpload :SparkMD5="SparkMD5" :request="request" :uploadApi="uploadApi" :mergeApi="mergeApi" :checkApi="checkApi" :fileList="videoFileList" :on-change="onChange" :on-remove="handleRemove" :on-progress="onProgress" :before-upload="beforeUpload" :on-exceed="onExceed" :on-success="onSuccess" :on-error="onError" :on-preview="onPreview" :on-reading-file="onReadingFile" :limit="10" :apiHeader="apiHeader" :accept='`mp4,avi,mov,wmv,3gp`' > <el-button type="primary" :disabled="disabledUpload" :loading="loadingUpload">{{loadingUpload ? $t('workGuide.FileLoading') : $t('workGuide.ClickToUpload') }}</el-button> <div slot="tip" class="el-upload__tip">只能上傳mp4,avi,mov,wmv,3gp文件,且不超過2G</div> </BigUpload> onChange(file,fileList) { this.disabledUpload = true; }, //讀取文件回調(diào),大文件讀取耗時較長,給一個loading狀態(tài)過度 onReadingFile(value){ value === 'start' ? this.loadingUpload = true : this.loadingUpload = false; }, beforeUpload(file) { const type = file.type.split('/')[0]; const isVideo = type === 'video'; const isLt2G = file.size / 1024 / 1024 < 2048; if (!isLt2G) { this.$message.error(this.$t('KOLProductBoard.MaximumSizeImages',{m:'2048'})) } else if (!isVideo) { this.$message.error(this.$t('workGuide.uploadFormatTip',{m:'mp4,avi,mov,wmv,3gp'})) } return isVideo && isLt2G; }, //超過最大上傳數(shù)量回調(diào) onExceed(file,fileList) { this.$message.warning(this.$t('KOLProductBoard.MaximumLimitImages', { m: '10'})); }, //上傳進度回調(diào) onProgress(file,percentage) { }, //預(yù)覽回調(diào) onPreview({url}) { this.bigImageUrl = url; this.showBigImage = true; }, onSuccess(file,url) { this.videoFileList.push(file); this.disabledUpload = false; this.$message.success(this.$t('KOL需求管理.UploadSuccessful')); }, onError(file,reason) { //reason 是在瀏覽器讀取文件失敗時特有的參數(shù) //禁用上傳 this.disabledUpload = false; if(reason) { this.loadingUpload = false; this.$message.error(reason); }else { this.$message.success(this.$t('workGuide.UploadFailed')); } }, handleRemove(file,fileList) { this.videoFileList = [...fileList]; },
這里有2個狀態(tài)"disabledUpload"(當(dāng)文件選擇后禁用上傳按鈕,知道上傳成功放開限制)"loadingUpload"(在讀取文件md5的過程中,開啟loading狀態(tài))都是通過不同的鉤子函數(shù)來控制
附源碼git地址
代碼沒有精簡,時間倉促,目前是使用在自己的項目中,有不完善和錯誤的地方,歡迎大家指出
以上就是基于element-ui自定義封裝大文件上傳組件的案例分享的詳細內(nèi)容,更多關(guān)于element-ui封裝大文件上傳組件的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
vue+iview?Table表格多選切換分頁保持勾選狀態(tài)
這篇文章主要為大家詳細介紹了vue+iview?Table表格多選切換分頁保持勾選狀態(tài),文中示例代碼介紹的非常詳細,具有一定的參考價值,感興趣的小伙伴們可以參考一下2022-07-07優(yōu)雅的將ElementUI表格變身成樹形表格的方法步驟
這篇文章主要介紹了優(yōu)雅的將ElementUI表格變身成樹形表格的方法步驟,文中通過示例代碼介紹的非常詳細,對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2019-04-04vuejs在解析時出現(xiàn)閃爍的原因及防止閃爍的方法
這篇文章主要介紹了vuejs在解析時出現(xiàn)閃爍的原因及防止閃爍的方法,本文介紹的非常詳細,具有參考借鑒價值,感興趣的朋友一起看看吧2016-09-09Vue3+Vite實現(xiàn)動態(tài)路由的詳細實例代碼
我們在開發(fā)大型系統(tǒng)的時候一般都需要動態(tài)添加路由,下面這篇文章主要給大家介紹了關(guān)于Vue3+Vite實現(xiàn)動態(tài)路由的相關(guān)資料,文中通過圖文以及實例代碼介紹的非常詳細,需要的朋友可以參考下2022-08-08解決antd datepicker 獲取時間默認(rèn)少8個小時的問題
這篇文章主要介紹了解決antd datepicker 獲取時間默認(rèn)少8個小時的問題,具有很好的參考價值,希望對大家有所幫助。一起跟隨小編過來看看吧2020-10-10