欧美bbbwbbbw肥妇,免费乱码人妻系列日韩,一级黄片

基于element-ui自定義封裝大文件上傳組件的案例分享

 更新時間:2024年01月31日 10:47:05   作者:張遼  
本文主要介紹了以element-ui基礎封裝大文件上傳的組件,包括斷點續(xù)傳,秒傳,上傳進度條,封裝思想邏輯來源于el-upload 組件源碼,文中有具體案例分享,需要的朋友可以參考下

以element-ui基礎封裝大文件上傳的組件,包括斷點續(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、文件按鈕部分,一個默認插槽加一個input框,默認插槽用來自定義上傳框的樣式,input大家都懂就是原生的上傳框,注意這個input 是需要隱藏的,這里偷懶直接用了element的類名
  • 2、上傳文件類型提示部分,一個文件類型提示的具名插槽 name="tip",用來自定義樣式給出提示的文案
  • 3、已上傳的文件列表,用來點擊預覽,刪除,以及上傳進度條的展示,進度條部分會有status ,是文件上傳的狀態(tài),當為uploading 時渲染

接下來是js 部分,分片部分的邏輯就不在這篇文章里面贅述了。

首先看組件的props

prop類型描述
beforeUploadFunction(file)文件上傳前鉤子上傳文件之前的鉤子,參數(shù)為上傳的文件,若返回 false 則停止上傳
onExceedFunction(file,fileList)文件超出個數(shù)限制時的鉤子
limitNumber文件限制數(shù)量
uploadApiString上傳文件的接口
mergeApiString文件上傳成功后,合并文件接口
checkApiString檢測文件上傳分片接口,返回已上傳所有片段的索引
acceptString允許上傳的文件類型
concurrentUploadBoolean是否允許并發(fā)請求(后端服務帶寬受限,可能需要同步依次上傳分片,而不是瞬間發(fā)起幾百個請求)
fileListArray上傳的文件列表, 例如: [{name: 'food.jpg', url: 'xxx.cdn.com/xxx.jpg'}]
onRemoveFunction(file,fileList)文件列表移除文件時的鉤子
onChangeFunction(file,fileList)文件狀態(tài)改變時的鉤子,添加文件時調用
onPreviewFunction(file)文件預覽鉤子
onSuccessFunction(file,url)文件合并成功鉤子,返回成功文件,和后端存儲到S3的url
onErrorFunction(file)文件上傳失敗鉤子
onProgressFunction(file,percentage)文件上傳進度鉤子
onReadingFileFunction(status)讀取文件md5時的鉤子函數(shù),參數(shù) start:開始讀取,end:讀取結束
chunckSizeNumber文件切片大小
requestFunction封裝好的axios,用于請求的工具函數(shù)
apiHeaderObject需要的特殊請求頭
SparkMD5Function讀取文件md5的工具函數(shù)(spark-md5直接安裝這個包)
multipleBoolean是否可多選文件(建議不多選,大文件有瓶頸)

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ù),如果超出限制,會直接終止當前邏輯并將文件,以及文件列表拋給父組件的onExceed 函數(shù),父組件自行給出提示,如果未超過限制,繼續(xù)執(zhí)行上傳邏輯執(zhí)行 upload 方法, upload 方法會調用beforeUpload 方法是否符合文件類型,如果返回ture, 繼續(xù)執(zhí)行,開始讀取大文件的md5(這里是關鍵)

繼續(xù)看readFile方法:

async readFile(files) {
    this.sliceFile(files);
    //注意這里,開始讀取文件,會回調父組件的onreadingFile,告訴組件開始讀取,此時父組件開始設置讀取的loading 狀態(tài),讀取完成之后再次調用會返回end表示讀取結束,此時將loading狀態(tài)改為false
    this.onReadingFile('start');
    const data = await this.getFileMD5(files);
    this.onReadingFile('end');
    //判斷是否上傳重復文件
    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; //同步上傳成功標記
        //斷點續(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ù)來算的,所以分母應該是未上傳的分片總數(shù)
                            if(!isSuccess) {
                                break;
                            }
                        }
                    }
                }
                //兼容并發(fā)與同步請求操作,受服務器帶寬影響,做并發(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ù)來算的,所以分母應該是未上傳的分片總數(shù)
                    if(!isSuccess) {
                        break;
                    }
                }
            }
            //兼容并發(fā)與同步請求操作,受服務器帶寬影響,做并發(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 方法來判斷,所有任務是否都為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中,接著開始讀取文件,這里可以自行在父組件中調用 onReadingFile 方法設置loading狀態(tài),提升用戶體驗度。 接著會直接調用服務單接口,檢查是否已經(jīng)上傳過,并將已上傳的分片序號寫入到一個isUploadChuncks list中,然后循環(huán)上傳未上片段,這里會執(zhí)行 onStart 方法,給每個文件一個初始對象,設置文件的初始狀態(tài),以及文件內(nèi)容,插入到已上傳的文件列表 uploadFiles 中,為后根據(jù)文件狀態(tài)展示進度條,以及上傳失敗時刪除對應文件列表做準備

劃重點:調用接口,處理上傳邏輯,這里主要分兩種。前面提到過,服務端會有上傳帶寬的限制,如果一次性發(fā)送很多的文件請求,服務端是接受不了的。所以分2種,并發(fā)上傳,和同步上傳。post 方法會返回一個promise,并生成了一個以每個promise請求,組成的promise 集合

//file:當前文件,nowChunck:當前分片索引,totalChunck:當前需要上傳文件分片的總數(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();
        // 當上傳完成時調用
        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匹配,然后修改對應文件的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;
},

//讀取文件回調,大文件讀取耗時較長,給一個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ù)量回調
onExceed(file,fileList) {
    this.$message.warning(this.$t('KOLProductBoard.MaximumLimitImages', { m: '10'}));
},

//上傳進度回調
onProgress(file,percentage) {

},

//預覽回調
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"(當文件選擇后禁用上傳按鈕,知道上傳成功放開限制)"loadingUpload"(在讀取文件md5的過程中,開啟loading狀態(tài))都是通過不同的鉤子函數(shù)來控制

附源碼git地址

代碼沒有精簡,時間倉促,目前是使用在自己的項目中,有不完善和錯誤的地方,歡迎大家指出

以上就是基于element-ui自定義封裝大文件上傳組件的案例分享的詳細內(nèi)容,更多關于element-ui封裝大文件上傳組件的資料請關注腳本之家其它相關文章!

相關文章

最新評論