Vue封裝一個簡單輕量的上傳文件組件的示例

一、之前遇到的一些問題

創(chuàng)新互聯(lián)從2013年成立,先為鐵山等服務(wù)建站,鐵山等地企業(yè),進(jìn)行企業(yè)商務(wù)咨詢服務(wù)。為鐵山企業(yè)網(wǎng)站制作PC+手機(jī)+微官網(wǎng)三網(wǎng)同步一站式服務(wù)解決您的所有建站問題。

項目中多出有上傳文件的需求,使用現(xiàn)有的UI框架實現(xiàn)的過程中,不知道什么原因,總會有一些莫名其妙的bug。比如用某上傳組件,明明注明(:multiple="false"),可實際上還是能多選,上傳的時候依然發(fā)送了多個文件;又比如只要加上了(:file-list="fileList")屬性,希望能手動控制上傳列表的時候,上傳事件this.refs.[upload(組件ref)].submit()就不起作用了,傳不了??傊?,懶得再看它怎么實現(xiàn)了,我用的是功能,界面本身還是要重寫的,如果堅持用也會使項目多很多不必要的邏輯、樣式代碼……

之前用Vue做項目用的視圖框架有element-ui,團(tuán)隊內(nèi)部作為補(bǔ)充的zp-ui,以及iview??蚣苁呛糜茫轻槍ψ约旱捻椖客荒苋磕脕碛?,尤其是我們的設(shè)計妹子出的界面與現(xiàn)有框架差異很大,改源碼效率低又容易導(dǎo)致未知的bug,于是自己就抽時間封裝了這個上傳組件。

二、代碼與介紹

父組件

<template>
 <div class="content">
 <label for="my-upload">
  <span>上傳</span>
 </label>
  <my-upload
   ref="myUpload"
   :file-list="fileList"
   action="/uploadPicture"
   :data="param"
   :on-change="onChange"
   :on-progress="uploadProgress"
   :on-success="uploadSuccess"
   :on-failed="uploadFailed"
   multiple
   :limit="5"
   :on-finished="onFinished">
  </my-upload>
  <button @click="upload" class="btn btn-xs btn-primary">Upload</button>
 </div>
</template>

<script>
import myUpload from './components/my-upload'
export default {
 name: 'test',
 data(){
  return {
  fileList: [],//上傳文件列表,無論單選還是支持多選,文件都以列表格式保存
  param: {param1: '', param2: '' },//攜帶參數(shù)列表
  }
 },
 methods: {
  onChange(fileList){//監(jiān)聽文件變化,增減文件時都會被子組件調(diào)用
  this.fileList = [...fileList];
  },
  uploadSuccess(index, response){//某個文件上傳成功都會執(zhí)行該方法,index代表列表中第index個文件
  console.log(index, response);
  },
  upload(){//觸發(fā)子組件的上傳方法
  this.$refs.myUpload.submit();
  },
  removeFile(index){//移除某文件
  this.$refs.myUpload.remove(index);
  },
  uploadProgress(index, progress){//上傳進(jìn)度,上傳時會不斷被觸發(fā),需要進(jìn)度指示時會很有用
  const{ percent } = progress;
  console.log(index, percent);
  },
  uploadFailed(index, err){//某文件上傳失敗會執(zhí)行,index代表列表中第index個文件
  console.log(index, err);
  },
  onFinished(result){//所有文件上傳完畢后(無論成?。﹫?zhí)行,result: { success: 成功數(shù)目, failed: 失敗數(shù)目 }
  console.log(result);
  }
 },
 components: {
  myUpload
 }
}
</script>

父組件處理與業(yè)務(wù)有關(guān)的邏輯,我特意加入索引參數(shù),便于界面展示上傳結(jié)果的時候能夠直接操作第幾個值,并不是所有方法都必須的,視需求使用。

子組件

<template>
<div>
 <input  @change="addFile" :multiple="multiple" type="file" :name="name" id="my-upload"/>
</div>
</template>

上傳文件,html部分就這么一對兒標(biāo)簽,不喜歡復(fù)雜啰嗦

<script>
export default {
 name: 'my-upload',
 props: {
 name: String,
 action: {
  type: String,
  required: true
 },
 fileList: {
  type: Array,
  default: []
 },
 data: Object,
 multiple: Boolean,
 limit: Number,
 onChange: Function,
 onBefore: Function,
 onProgress: Function,
 onSuccess: Function,
 onFailed: Function,
 onFinished: Function
 },
 methods: {}//下文主要是methods的介紹,此處先省略
}
</script>

這里定義了父組件向子組件需要傳遞的屬性值,注意,這里把方法也當(dāng)做了屬性傳遞,都是可以的。

自己寫的組件,沒有像流行框架發(fā)布的那樣完備和全面,另外針對開頭提到的綁定file-list就不能上傳了的問題(更可能是我的姿勢不對),本人也想極力解決掉自身遇到的這個問題,所以希望能對文件列表有絕對的控制權(quán),除了action,把file-list也作為父組件必須要傳遞的屬性。(屬性名父組件使用“-”連接,對應(yīng)子組件prop中的駝峰命名)

三、主要的上傳功能

methods: {
  addFile, remove, submit, checkIfCanUpload
}

methods內(nèi)一共4個方法,添加文件、移除文件、提交、檢測(上傳之前的檢驗),下面一一講述:

1.添加文件

addFile({target: {files}}){//input標(biāo)簽觸發(fā)onchange事件時,將文件加入待上傳列表
 for(let i = 0, l = files.length; i < l; i++){
 files[i].url = URL.createObjectURL(files[i]);//創(chuàng)建blob地址,不然圖片怎么展示?
 files[i].status = 'ready';//開始想給文件一個字段表示上傳進(jìn)行的步驟的,后面好像也沒去用......
 }
 let fileList = [...this.fileList];
 if(this.multiple){//多選時,文件全部壓如列表末尾
 fileList = [...fileList, ...files];
 let l = fileList.length;
 let limit = this.limit;
 if(limit && typeof limit === "number" && Math.ceil(limit) > 0 && l > limit){//有數(shù)目限制時,取后面limit個文件
  limit = Math.ceil(limit);
//  limit = limit > 10 ? 10 : limit;
  fileList = fileList.slice(l - limit);
 }
 }else{//單選時,只取最后一個文件。注意這里沒寫成fileList = files;是因為files本身就有多個元素(比如選擇文件時一下子框了一堆)時,也只要一個
 fileList = [files[0]];
 }
 this.onChange(fileList);//調(diào)用父組件方法,將列表緩存到上一級data中的fileList屬性
 },

2.移除文件

這個簡單,有時候在父組件叉掉某文件的時候,傳一個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對象不存在的時候,使用fetch請求上傳邏輯會更簡單一些

submit(){
 if(this.checkIfCanUpload()){
 if(this.onProgress && typeof XMLHttpRequest !== 'undefined')
  this.xhrSubmit();
 else
  this.fetchSubmit();
 }
},

4.基于上傳的兩套邏輯,這里封裝了兩個方法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ù)返回值類型使用的,應(yīng)該視情況而定
 });
 Promise.all(promises).then(resArray => {//多線程同時開始,如果并發(fā)數(shù)有限制,可以使用同步的方式一個一個傳,這里不再贅述。
 let success = 0, failed = 0;
 resArray.forEach((res, index) => {
  if(res.code == 1){
  success++;         //統(tǒng)計上傳成功的個數(shù),由索引可以知道哪些成功了
  this.onSuccess(index, res);
  }else if(res.code == 520){   //約定失敗的返回值是520
  failed++;         //統(tǒng)計上傳失敗的個數(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]);//這里用了個異步方法,按次序執(zhí)行this.sendRequest方法,參數(shù)為文件列表包裝的每個對象,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;
 }
}

最后把請求前的校驗加上

checkIfCanUpload(){
 return this.fileList.length ? (this.onBefore && this.onBefore() || !this.onBefore) : false;
},

如果父組件定義了onBefore方法且返回了false,或者文件列表為空,請求就不會發(fā)送。

代碼部分完了,使用時只要有了on-progress屬性并且XMLHttpRequest對象可訪問,就會使用原生方式發(fā)送請求,否則就用fetch發(fā)送請求(不展示進(jìn)度)。

以上就是本文的全部內(nèi)容,希望對大家的學(xué)習(xí)有所幫助,也希望大家多多支持創(chuàng)新互聯(lián)。

分享文章:Vue封裝一個簡單輕量的上傳文件組件的示例
網(wǎng)頁鏈接:http://bm7419.com/article0/gocgoo.html

成都網(wǎng)站建設(shè)公司_創(chuàng)新互聯(lián),為您提供標(biāo)簽優(yōu)化、電子商務(wù)、網(wǎng)站設(shè)計網(wǎng)站排名、小程序開發(fā)、品牌網(wǎng)站制作

廣告

聲明:本網(wǎng)站發(fā)布的內(nèi)容(圖片、視頻和文字)以用戶投稿、用戶轉(zhuǎn)載內(nèi)容為主,如果涉及侵權(quán)請盡快告知,我們將會在第一時間刪除。文章觀點不代表本網(wǎng)站立場,如需處理請聯(lián)系客服。電話:028-86922220;郵箱:631063699@qq.com。內(nèi)容未經(jīng)允許不得轉(zhuǎn)載,或轉(zhuǎn)載時需注明來源: 創(chuàng)新互聯(lián)

h5響應(yīng)式網(wǎng)站建設(shè)