Vue封装一个简单轻量的上传文件组件的示例


Posted in Javascript onMarch 21, 2018

一、之前遇到的一些问题

项目中多出有上传文件的需求,使用现有的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-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: '' },//携带参数列表
  }
 },
 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部分就这么一对儿标签,不喜欢复杂???/p>

<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,或者文件列表为空,请求就不会发送。

代码部分完了,使用时只要有了on-progress属性并且XMLHttpRequest对象可访问,就会使用原生方式发送请求,否则就用fetch发送请求(不展示进度)。

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持三水点靠木。

Javascript 相关文章推荐
document 和 document.all 分别什么时候用
Jun 22 Javascript
JavaScript判断两种格式的输入日期的正确性的代码
Mar 25 Javascript
jquery实现可拖动DIV自定义保存到数据的实例
Nov 20 Javascript
利用JQuery制作符合Web标准的QQ弹出消息
Jan 14 Javascript
js实现发送验证码后的倒计时功能
May 28 Javascript
举例说明JavaScript中的实例对象与原型对象
Mar 11 Javascript
JavaScript实现图片无缝滚动效果
Jul 07 Javascript
Angular4集成ng2-file-upload的上传组件
Mar 14 Javascript
JS/HTML5游戏常用算法之碰撞检测 地图格子算法实例详解
Dec 12 Javascript
Vue 通过公共字段,拼接两个对象数组的实例
Nov 07 Javascript
解决vue里a标签值解析变量,跳转页面,前面加默认域名端口的问题
Jul 22 Javascript
Javascript 模拟mvc实现点餐程序案例详解
Dec 24 Javascript
基于vue-video-player自定义播放器的方法
Mar 21 #Javascript
基于iScroll实现内容滚动效果
Mar 21 #Javascript
JS中的回调函数实例浅析
Mar 21 #Javascript
使用Vue制作图片轮播组件思路详解
Mar 21 #Javascript
JS实现为动态添加的元素增加事件功能示例【基于事件委托】
Mar 21 #Javascript
JS实现遍历不规则多维数组的方法
Mar 21 #Javascript
vue项目关闭eslint校验
Mar 21 #Javascript
You might like
php简单判断两个字符串是否相等的方法
2015/07/13 PHP
Zend Framework教程之Resource Autoloading用法实例
2016/03/08 PHP
Yii实现Command任务处理的方法详解
2016/07/14 PHP
PHP中spl_autoload_register()函数用法实例详解
2016/07/18 PHP
php输出反斜杠的实例方法
2019/09/19 PHP
PHP使用PDO 连接与连接管理操作实例分析
2020/04/21 PHP
基于php解决json_encode中文UNICODE转码问题
2020/11/10 PHP
jQuery 行级解析读取XML文件(附源码)
2009/10/12 Javascript
EditPlus注册码生成器(js代码实现)
2013/03/25 Javascript
javascript日期计算实例分析
2015/06/29 Javascript
js代码实现无缝滚动(文字和图片)
2015/08/20 Javascript
使用EVAL处理jqchart jquery 折线图返回数据无效的解决办法
2015/11/26 Javascript
JavaScript定义函数_动力节点Java学院整理
2017/06/27 Javascript
vue.js实现刷新当前页面的方法教程
2017/07/05 Javascript
vue router 组件的高级应用实例代码
2019/04/08 Javascript
浅谈react-router@4.0 使用方法和源码分析
2019/06/04 Javascript
基于Vue的商品主图放大镜方案详解
2019/09/19 Javascript
python检查URL是否正常访问的小技巧
2017/02/25 Python
轻量级的Web框架Flask 中模块化应用的实现
2017/09/11 Python
Python的赋值、深拷贝与浅拷贝的区别详解
2020/02/12 Python
python3中for循环踩过的坑记录
2020/12/14 Python
python上下文管理的使用场景实例讲解
2021/03/03 Python
肯尼亚网上商城:Kilimall
2016/08/20 全球购物
KIKO MILANO荷兰网上商店:意大利专业化妆品品牌
2017/05/12 全球购物
英语专业毕业生求职简历的自我评价
2013/10/24 职场文书
师范应届生教师求职信
2013/11/05 职场文书
2014三八妇女节活动总结
2014/03/01 职场文书
煤矿安全保证书
2015/02/27 职场文书
财务稽核岗位职责
2015/04/13 职场文书
车辆管理制度范本
2015/08/05 职场文书
九年级语文教学反思
2016/03/03 职场文书
Python Numpy之linspace用法说明
2021/04/17 Python
基于Python实现射击小游戏的制作
2022/04/06 Python
vue3.0 数字翻牌组件的使用方法详解
2022/04/20 Vue.js
SQL使用复合索引实现数据库查询的优化
2022/05/25 SQL Server
python库Tsmoothie模块数据平滑化异常点抓取
2022/06/10 Python