一、之前遇到的一些問(wèn)題
創(chuàng)新互聯(lián)從2013年成立,先為鐵山等服務(wù)建站,鐵山等地企業(yè),進(jìn)行企業(yè)商務(wù)咨詢服務(wù)。為鐵山企業(yè)網(wǎng)站制作PC+手機(jī)+微官網(wǎng)三網(wǎng)同步一站式服務(wù)解決您的所有建站問(wèn)題。
項(xiàng)目中多出有上傳文件的需求,使用現(xiàn)有的UI框架實(shí)現(xiàn)的過(guò)程中,不知道什么原因,總會(huì)有一些莫名其妙的bug。比如用某上傳組件,明明注明(:multiple="false"),可實(shí)際上還是能多選,上傳的時(shí)候依然發(fā)送了多個(gè)文件;又比如只要加上了(:file-list="fileList")屬性,希望能手動(dòng)控制上傳列表的時(shí)候,上傳事件this.refs.[upload(組件ref)].submit()就不起作用了,傳不了??傊?,懶得再看它怎么實(shí)現(xiàn)了,我用的是功能,界面本身還是要重寫(xiě)的,如果堅(jiān)持用也會(huì)使項(xiàng)目多很多不必要的邏輯、樣式代碼……
之前用Vue做項(xiàng)目用的視圖框架有element-ui,團(tuán)隊(duì)內(nèi)部作為補(bǔ)充的zp-ui,以及iview??蚣苁呛糜?,但是針對(duì)自己的項(xiàng)目往往不能全部拿來(lái)用,尤其是我們的設(shè)計(jì)妹子出的界面與現(xiàn)有框架差異很大,改源碼效率低又容易導(dǎo)致未知的bug,于是自己就抽時(shí)間封裝了這個(gè)上傳組件。
二、代碼與介紹
父組件
父組件處理與業(yè)務(wù)有關(guān)的邏輯,我特意加入索引參數(shù),便于界面展示上傳結(jié)果的時(shí)候能夠直接操作第幾個(gè)值,并不是所有方法都必須的,視需求使用。
子組件
上傳文件,html部分就這么一對(duì)兒標(biāo)簽,不喜歡復(fù)雜啰嗦
這里定義了父組件向子組件需要傳遞的屬性值,注意,這里把方法也當(dāng)做了屬性傳遞,都是可以的。
自己寫(xiě)的組件,沒(méi)有像流行框架發(fā)布的那樣完備和全面,另外針對(duì)開(kāi)頭提到的綁定file-list就不能上傳了的問(wèn)題(更可能是我的姿勢(shì)不對(duì)),本人也想極力解決掉自身遇到的這個(gè)問(wèn)題,所以希望能對(duì)文件列表有絕對(duì)的控制權(quán),除了action,把file-list也作為父組件必須要傳遞的屬性。(屬性名父組件使用“-”連接,對(duì)應(yīng)子組件prop中的駝峰命名)
三、主要的上傳功能
methods: { addFile, remove, submit, checkIfCanUpload }
methods內(nèi)一共4個(gè)方法,添加文件、移除文件、提交、檢測(cè)(上傳之前的檢驗(yàn)),下面一一講述:
1.添加文件
addFile({target: {files}}){//input標(biāo)簽觸發(fā)onchange事件時(shí),將文件加入待上傳列表 for(let i = 0, l = files.length; i < l; i++){ files[i].url = URL.createObjectURL(files[i]);//創(chuàng)建blob地址,不然圖片怎么展示? files[i].status = 'ready';//開(kāi)始想給文件一個(gè)字段表示上傳進(jìn)行的步驟的,后面好像也沒(méi)去用...... } let fileList = [...this.fileList]; if(this.multiple){//多選時(shí),文件全部壓如列表末尾 fileList = [...fileList, ...files]; let l = fileList.length; let limit = this.limit; if(limit && typeof limit === "number" && Math.ceil(limit) > 0 && l > limit){//有數(shù)目限制時(shí),取后面limit個(gè)文件 limit = Math.ceil(limit); // limit = limit > 10 ? 10 : limit; fileList = fileList.slice(l - limit); } }else{//單選時(shí),只取最后一個(gè)文件。注意這里沒(méi)寫(xiě)成fileList = files;是因?yàn)閒iles本身就有多個(gè)元素(比如選擇文件時(shí)一下子框了一堆)時(shí),也只要一個(gè) fileList = [files[0]]; } this.onChange(fileList);//調(diào)用父組件方法,將列表緩存到上一級(jí)data中的fileList屬性 },
2.移除文件
這個(gè)簡(jiǎn)單,有時(shí)候在父組件叉掉某文件的時(shí)候,傳一個(gè)index即可。
remove(index){ let fileList = [...this.fileList]; if(fileList.length){ fileList.splice(index, 1); this.onChange(fileList); } },
3.提交上傳
這里使用了兩種方式,fetch和原生方式,由于fetch不支持獲取上傳的進(jìn)度,如果不需要進(jìn)度條或者自己模擬進(jìn)度或者XMLHttpRequest對(duì)象不存在的時(shí)候,使用fetch請(qǐng)求上傳邏輯會(huì)更簡(jiǎn)單一些
submit(){ if(this.checkIfCanUpload()){ if(this.onProgress && typeof XMLHttpRequest !== 'undefined') this.xhrSubmit(); else this.fetchSubmit(); } },
4.基于上傳的兩套邏輯,這里封裝了兩個(gè)方法xhrSubmit和fetchSubmit
fetchSubmit
fetchSubmit(){ let keys = Object.keys(this.data), values = Object.values(this.data), action = this.action; const promises = this.fileList.map(each => { each.status = "uploading"; let data = new FormData(); data.append(this.name || 'file', each); keys.forEach((one, index) => data.append(one, values[index])); return fetch(action, { method: 'POST', headers: { "Content-Type" : "application/x-www-form-urlencoded" }, body: data }).then(res => res.text()).then(res => JSON.parse(res));//這里res.text()是根據(jù)返回值類(lèi)型使用的,應(yīng)該視情況而定 }); Promise.all(promises).then(resArray => {//多線程同時(shí)開(kāi)始,如果并發(fā)數(shù)有限制,可以使用同步的方式一個(gè)一個(gè)傳,這里不再贅述。 let success = 0, failed = 0; resArray.forEach((res, index) => { if(res.code == 1){ success++; //統(tǒng)計(jì)上傳成功的個(gè)數(shù),由索引可以知道哪些成功了 this.onSuccess(index, res); }else if(res.code == 520){ //約定失敗的返回值是520 failed++; //統(tǒng)計(jì)上傳失敗的個(gè)數(shù),由索引可以知道哪些失敗了 this.onFailed(index, res); } }); return { success, failed }; //上傳結(jié)束,將結(jié)果傳遞到下文 }).then(this.onFinished); //把上傳總結(jié)果返回 },
xhrSubmit
xhrSubmit(){ const _this = this; let options = this.fileList.map((rawFile, index) => ({ file: rawFile, data: _this.data, filename: _this.name || "file", action: _this.action, onProgress(e){ _this.onProgress(index, e);//閉包,將index存住 }, onSuccess(res){ _this.onSuccess(index, res); }, onError(err){ _this.onFailed(index, err); } })); let l = this.fileList.length; let send = async options => { for(let i = 0; i < l; i++){ await _this.sendRequest(options[i]);//這里用了個(gè)異步方法,按次序執(zhí)行this.sendRequest方法,參數(shù)為文件列表包裝的每個(gè)對(duì)象,this.sendRequest下面緊接著介紹 } }; send(options); },
這里借鑒了element-ui的上傳源碼
sendRequest(option){ const _this = this; upload(option); function getError(action, option, xhr) { var msg = void 0; if (xhr.response) { msg = xhr.status + ' ' + (xhr.response.error || xhr.response); } else if (xhr.responseText) { msg = xhr.status + ' ' + xhr.responseText; } else { msg = 'fail to post ' + action + ' ' + xhr.status; } var err = new Error(msg); err.status = xhr.status; err.method = 'post'; err.url = action; return err; } function getBody(xhr) { var text = xhr.responseText || xhr.response; if (!text) { return text; } try { return JSON.parse(text); } catch (e) { return text; } } function upload(option) { if (typeof XMLHttpRequest === 'undefined') { return; } var xhr = new XMLHttpRequest(); var action = option.action; if (xhr.upload) { xhr.upload.onprogress = function progress(e) { if (e.total > 0) { e.percent = e.loaded / e.total * 100; } option.onProgress(e); }; } var formData = new FormData(); if (option.data) { Object.keys(option.data).map(function (key) { formData.append(key, option.data[key]); }); } formData.append(option.filename, option.file); xhr.onerror = function error(e) { option.onError(e); }; xhr.onload = function onload() { if (xhr.status < 200 || xhr.status >= 300) { return option.onError(getError(action, option, xhr)); } option.onSuccess(getBody(xhr)); }; xhr.open('post', action, true); if (option.withCredentials && 'withCredentials' in xhr) { xhr.withCredentials = true; } var headers = option.headers || {}; for (var item in headers) { if (headers.hasOwnProperty(item) && headers[item] !== null) { xhr.setRequestHeader(item, headers[item]); } } xhr.send(formData); return xhr; } }
最后把請(qǐng)求前的校驗(yàn)加上
checkIfCanUpload(){ return this.fileList.length ? (this.onBefore && this.onBefore() || !this.onBefore) : false; },
如果父組件定義了onBefore方法且返回了false,或者文件列表為空,請(qǐng)求就不會(huì)發(fā)送。
代碼部分完了,使用時(shí)只要有了on-progress屬性并且XMLHttpRequest對(duì)象可訪問(wèn),就會(huì)使用原生方式發(fā)送請(qǐng)求,否則就用fetch發(fā)送請(qǐng)求(不展示進(jìn)度)。
以上就是本文的全部?jī)?nèi)容,希望對(duì)大家的學(xué)習(xí)有所幫助,也希望大家多多支持創(chuàng)新互聯(lián)。