Vue封装一个简单轻量的上传文件组件的示例
一、之前遇到的一些问题
项目中多出有上传文件的需求,使用现有的UI框架实现的过程中,不知道什么原因,总会有一些莫名其妙的bug。比如用某上传组件,明明注明(:multiple="false"),可实际上还是能多选,上传的时候依然发送了多个文件;又比如只要加上了(:file-list="fileList")属性,希望能手动控制上传列表的时候,上传事件this.refs.[upload(组件ref)].submit()就不起作用了,传不了。总之,懒得再看它怎么实现了,我用的是功能,界面本身还是要重写的,如果坚持用也会使项目多很多不必要的逻辑、样式代码……
之前用Vue做项目用的视图框架有element-ui,团队内部作为补充的zp-ui,以及iview。框架是好用,但是针对自己的项目往往不能全部拿来用,尤其是我们的设计妹子出的界面与现有框架差异很大,改源码效率低又容易导致未知的bug,于是自己就抽时间封装了这个上传组件。
二、代码与介绍
父组件
上传
父组件处理与业务有关的逻辑,我特意加入索引参数,便于界面展示上传结果的时候能够直接操作第几个值,并不是所有方法都必须的,视需求使用。
子组件
上传文件,html部分就这么一对儿标签,不喜欢复杂啰嗦
这里定义了父组件向子组件需要传递的属性值,注意,这里把方法也当做了属性传递,都是可以的。
自己写的组件,没有像流行框架发布的那样完备和全面,另外针对开头提到的绑定file-list就不能上传了的问题(更可能是我的姿势不对),本人也想极力解决掉自身遇到的这个问题,所以希望能对文件列表有绝对的控制权,除了action,把file-list也作为父组件必须要传递的属性。(属性名父组件使用“-”连接,对应子组件prop中的驼峰命名)
三、主要的上传功能
methods:{ addFile,remove,submit,checkIfCanUpload }
methods内一共4个方法,添加文件、移除文件、提交、检测(上传之前的检验),下面一一讲述:
1.添加文件
addFile({target:{files}}){//input标签触发onchange事件时,将文件加入待上传列表 for(leti=0,l=files.length;i0&&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){ letfileList=[...this.fileList]; if(fileList.length){ fileList.splice(index,1); this.onChange(fileList); } },
3.提交上传
这里使用了两种方式,fetch和原生方式,由于fetch不支持获取上传的进度,如果不需要进度条或者自己模拟进度或者XMLHttpRequest对象不存在的时候,使用fetch请求上传逻辑会更简单一些
submit(){ if(this.checkIfCanUpload()){ if(this.onProgress&&typeofXMLHttpRequest!=='undefined') this.xhrSubmit(); else this.fetchSubmit(); } },
4.基于上传的两套逻辑,这里封装了两个方法xhrSubmit和fetchSubmit
fetchSubmit
fetchSubmit(){ letkeys=Object.keys(this.data),values=Object.values(this.data),action=this.action; constpromises=this.fileList.map(each=>{ each.status="uploading"; letdata=newFormData(); data.append(this.name||'file',each); keys.forEach((one,index)=>data.append(one,values[index])); returnfetch(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=>{//多线程同时开始,如果并发数有限制,可以使用同步的方式一个一个传,这里不再赘述。 letsuccess=0,failed=0; resArray.forEach((res,index)=>{ if(res.code==1){ success++;//统计上传成功的个数,由索引可以知道哪些成功了 this.onSuccess(index,res); }elseif(res.code==520){//约定失败的返回值是520 failed++;//统计上传失败的个数,由索引可以知道哪些失败了 this.onFailed(index,res); } }); return{success,failed};//上传结束,将结果传递到下文 }).then(this.onFinished);//把上传总结果返回 },
xhrSubmit
xhrSubmit(){ const_this=this; letoptions=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); } })); letl=this.fileList.length; letsend=asyncoptions=>{ for(leti=0;i这里借鉴了element-ui的上传源码
sendRequest(option){ const_this=this; upload(option); functiongetError(action,option,xhr){ varmsg=void0; if(xhr.response){ msg=xhr.status+''+(xhr.response.error||xhr.response); }elseif(xhr.responseText){ msg=xhr.status+''+xhr.responseText; }else{ msg='failtopost'+action+''+xhr.status; } varerr=newError(msg); err.status=xhr.status; err.method='post'; err.url=action; returnerr; } functiongetBody(xhr){ vartext=xhr.responseText||xhr.response; if(!text){ returntext; } try{ returnJSON.parse(text); }catch(e){ returntext; } } functionupload(option){ if(typeofXMLHttpRequest==='undefined'){ return; } varxhr=newXMLHttpRequest(); varaction=option.action; if(xhr.upload){ xhr.upload.onprogress=functionprogress(e){ if(e.total>0){ e.percent=e.loaded/e.total*100; } option.onProgress(e); }; } varformData=newFormData(); if(option.data){ Object.keys(option.data).map(function(key){ formData.append(key,option.data[key]); }); } formData.append(option.filename,option.file); xhr.onerror=functionerror(e){ option.onError(e); }; xhr.onload=functiononload(){ if(xhr.status<200||xhr.status>=300){ returnoption.onError(getError(action,option,xhr)); } option.onSuccess(getBody(xhr)); }; xhr.open('post',action,true); if(option.withCredentials&&'withCredentials'inxhr){ xhr.withCredentials=true; } varheaders=option.headers||{}; for(variteminheaders){ if(headers.hasOwnProperty(item)&&headers[item]!==null){ xhr.setRequestHeader(item,headers[item]); } } xhr.send(formData); returnxhr; } }最后把请求前的校验加上
checkIfCanUpload(){ returnthis.fileList.length?(this.onBefore&&this.onBefore()||!this.onBefore):false; },如果父组件定义了onBefore方法且返回了false,或者文件列表为空,请求就不会发送。
代码部分完了,使用时只要有了on-progress属性并且XMLHttpRequest对象可访问,就会使用原生方式发送请求,否则就用fetch发送请求(不展示进度)。
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持毛票票。