视频1 视频21 视频41 视频61 视频文章1 视频文章21 视频文章41 视频文章61 推荐1 推荐3 推荐5 推荐7 推荐9 推荐11 推荐13 推荐15 推荐17 推荐19 推荐21 推荐23 推荐25 推荐27 推荐29 推荐31 推荐33 推荐35 推荐37 推荐39 推荐41 推荐43 推荐45 推荐47 推荐49 关键词1 关键词101 关键词201 关键词301 关键词401 关键词501 关键词601 关键词701 关键词801 关键词901 关键词1001 关键词1101 关键词1201 关键词1301 关键词1401 关键词1501 关键词1601 关键词1701 关键词1801 关键词1901 视频扩展1 视频扩展6 视频扩展11 视频扩展16 文章1 文章201 文章401 文章601 文章801 文章1001 资讯1 资讯501 资讯1001 资讯1501 标签1 标签501 标签1001 关键词1 关键词501 关键词1001 关键词1501 专题2001
vue封装轻量级文件上传组件
2020-11-27 19:37:11 责编:小采
文档

这次给大家带来vue封装轻量级文件上传组件,vue封装轻量级文件上传组件的注意事项有哪些,下面就是实战案例,一起来看一下。

一、之前遇到的一些问题

项目中多出有上传文件的需求,使用现有的UI框架实现的过程中,不知道什么原因,总会有一些莫名其妙的bug。比如用某上传组件,明明注明(:multiple="false"),可实际上还是能多选,上传的时候依然发送了多个文件;又比如只要加上了(:file-list="fileList")属性,希望能手动控制上传列表的时候,上传事件this.refs.[upload(组件ref)].submit()就不起作用了,传不了。总之,懒得再看它怎么实现了,我用的是功能,界面本身还是要重写的,如果坚持用也会使项目多很多不必要的逻辑、样式代码……

之前用Vue做项目用的视图框架有element-ui,团队内部作为补充的zp-ui,以及iview。框架是好用,但是针对自己的项目往往不能全部拿来用,尤其是我们的设计妹子出的界面与现有框架差异很大,改源码效率低又容易导致未知的bug,于是自己就抽时间封装了这个上传组件。

二、代码与介绍

父组件

<template>
 <p 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>
 </p>
</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>
<p>
 <input style="display:none" @change="addFile" :multiple="multiple" type="file" :name="name" id="my-upload"/>
</p>
</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();
 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()是根据返回值类型使用的,应该视情况而定
 });
 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){ //约定失败的返回值是520
 failed++; //统计上传失败的个数,由索引可以知道哪些失败了
 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,或者文件列表为空,请求就不会发送。

相信看了本文案例你已经掌握了方法,更多精彩请关注Gxl网其它相关文章!

推荐阅读:

p5.js使用draw()函数做出黄金螺旋

实战项目中如何使用jquery filter()

下载本文
显示全文
专题