Vue封装一个简单轻量的上传文件组件
一、之前遇到的一些問題
項目中多出有上傳文件的需求,使用現有的UI框架實現的過程中,不知道什么原因,總會有一些莫名其妙的bug。比如用某上傳組件,明明注明(:multiple="false"),可實際上還是能多選,上傳的時候依然發送了多個文件;又比如只要加上了(:file-list="fileList")屬性,希望能手動控制上傳列表的時候,上傳事件this.refs.[upload(組件ref)].submit()就不起作用了,傳不了。總之,懶得再看它怎么實現了,我用的是功能,界面本身還是要重寫的,如果堅持用也會使項目多很多不必要的邏輯、樣式代碼……
之前用Vue做項目用的視圖框架有element-ui,團隊內部作為補充的zp-ui,以及iview。框架是好用,但是針對自己的項目往往不能全部拿來用,尤其是我們的設計妹子出的界面與現有框架差異很大,改源碼效率低又容易導致未知的bug,于是自己就抽時間封裝了這個上傳組件。
二、代碼與介紹
父組件
<template><div class="content"><label for="my-upload"><span>上傳</span></label><my-uploadref="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: '' },//攜帶參數列表}},methods: {onChange(fileList){//監聽文件變化,增減文件時都會被子組件調用this.fileList = [...fileList];},uploadSuccess(index, response){//某個文件上傳成功都會執行該方法,index代表列表中第index個文件console.log(index, response);},upload(){//觸發子組件的上傳方法this.$refs.myUpload.submit();},removeFile(index){//移除某文件this.$refs.myUpload.remove(index);},uploadProgress(index, progress){//上傳進度,上傳時會不斷被觸發,需要進度指示時會很有用const{ percent } = progress;console.log(index, percent);},uploadFailed(index, err){//某文件上傳失敗會執行,index代表列表中第index個文件console.log(index, err);},onFinished(result){//所有文件上傳完畢后(無論成敗)執行,result: { success: 成功數目, failed: 失敗數目 }console.log(result);}},components: {myUpload} } </script>復制代碼父組件處理與業務有關的邏輯,我特意加入索引參數,便于界面展示上傳結果的時候能夠直接操作第幾個值,并不是所有方法都必須的,視需求使用。
子組件
<template> <div><input style="display:none" @change="addFile" :multiple="multiple" type="file" :name="name" id="my-upload"/> </div> </template>復制代碼上傳文件,html部分就這么一對兒標簽,不喜歡復雜啰嗦
<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>復制代碼這里定義了父組件向子組件需要傳遞的屬性值,注意,這里把方法也當做了屬性傳遞,都是可以的。
自己寫的組件,沒有像流行框架發布的那樣完備和全面,另外針對開頭提到的綁定file-list就不能上傳了的問題(更可能是我的姿勢不對),本人也想極力解決掉自身遇到的這個問題,所以希望能對文件列表有絕對的控制權,除了action,把file-list也作為父組件必須要傳遞的屬性。(屬性名父組件使用“-”連接,對應子組件prop中的駝峰命名)
三、主要的上傳功能
methods: {addFile, remove, submit, checkIfCanUpload } 復制代碼methods內一共4個方法,添加文件、移除文件、提交、檢測(上傳之前的檢驗),下面一一講述:
1.添加文件
addFile({target: {files}}){//input標簽觸發onchange事件時,將文件加入待上傳列表for(let i = 0, l = files.length; i < l; i++){files[i].url = URL.createObjectURL(files[i]);//創建blob地址,不然圖片怎么展示?files[i].status = 'ready';//開始想給文件一個字段表示上傳進行的步驟的,后面好像也沒去用......}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){//有數目限制時,取后面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);//調用父組件方法,將列表緩存到上一級data中的fileList屬性},復制代碼2.移除文件
這個簡單,有時候在父組件叉掉某文件的時候,傳一個index即可。
remove(index){let fileList = [...this.fileList];if(fileList.length){fileList.splice(index, 1);this.onChange(fileList);} },復制代碼3.提交上傳
這里使用了兩種方式,fetch和原生方式,由于fetch不支持獲取上傳的進度,如果不需要進度條或者自己模擬進度或者XMLHttpRequest對象不存在的時候,使用fetch請求上傳邏輯會更簡單一些
submit(){if(this.checkIfCanUpload()){if(this.onProgress && typeof XMLHttpRequest !== 'undefined')this.xhrSubmit();elsethis.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()是根據返回值類型使用的,應該視情況而定});Promise.all(promises).then(resArray => {//多線程同時開始,如果并發數有限制,可以使用同步的方式一個一個傳,這里不再贅述。let success = 0, failed = 0;resArray.forEach((res, index) => {if(res.code == 1){success++; //統計上傳成功的個數,由索引可以知道哪些成功了this.onSuccess(index, res);}else if(res.code == 520){ //約定失敗的返回值是520failed++; //統計上傳失敗的個數,由索引可以知道哪些失敗了this.onFailed(index, res);}});return { success, failed }; //上傳結束,將結果傳遞到下文}).then(this.onFinished); //把上傳總結果返回 },復制代碼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]);//這里用了個異步方法,按次序執行this.sendRequest方法,參數為文件列表包裝的每個對象,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,或者文件列表為空,請求就不會發送。
代碼部分完了,使用時只要有了on-progress屬性并且XMLHttpRequest對象可訪問,就會使用原生方式發送請求,否則就用fetch發送請求(不展示進度)。
轉載于:https://juejin.im/post/5ab0a40af265da239f073331
總結
以上是生活随笔為你收集整理的Vue封装一个简单轻量的上传文件组件的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Cocos Creator中按钮组件数组
- 下一篇: Promise详解(一) ----基础用