基于element 图片上传封装

2,014 阅读6分钟

我们用过element ui文件上传的朋友都知道,它属性齐全,api强大,但是我们在好多后台系统上传的时候发现,每一个上传我们都需要重新配置一遍,action,headers,data,name,以及上传前校验,上传个数等等。烦不胜烦。有没有办法就写一个图片上传,一个上传方法,其他的通过简单地配置就能实现上传呢,答案当然是有的。今天就带大家来实现一个单图,多图上传封装,其中还包含了图片预览,格式校验,以及图片删除,数据返回类型等操作,满足你的基本要求。本次封装不包含裁剪以及视频和文件上传。这些功能在接下来会逐步完成,希望大家喜欢,先上图!

image.png 我们成功的上传了一张图片,我们再来看一下上传成功后的控制台显示的源文件内容

image.png 我们发现,先是第一行和最后一行的WebKitFormBoundary 码,第二行的ContentDisposition,该行包含一些文件基本信息,还有第三行文件内容类型,后端的同志通过我们传递的文件数据规则,来进行文件解析, 传递的数据规则里包含所传递文件的基本信息 ,如文件名与文件类型,以便后端写出正确格式的文件。 到这里,我们明白了大致的上传原理。接下来,我们来订制一下专属我们的上传组件流程。

首先,我们使用的是axios,不需要像ajax一样自己去通过FormData实例化一个文件fd插入到文件内容中去,我们只需要在element提供的文件上传headers属性中添加进去'ContentType': 'multipart/form-data'就可以实现上传formdata编码类型的传输了。至于为什么要用formdata类型,涉及到二进制文件编码云云,我也是半知半解,想详细了解的同学可以自行查阅,这里不多说。 然后我们就开始写组件的属性,把常用的upload属性都拿过来

props:{
	value:{},
	//action是上传的地址
	action:{
		type:String,
		default: process.env.VUE_APP_BASE_API + process.env.VUE_APP_MY_UPLOAD_URL  || ''
	},
	//最大允许上传个数
	limit: {
		type: Number,
		default: 20
	},
	//是否采用头像上传模式
	avatar:{
		type:Boolean,
		default:false,
	},
	//根据业务需要,设置是否返回给父组件字符串类型
	//或者直接返回数组
	isString:{
		type:Boolean,
		default:false,
	},
	//是否显示已上传文件列表,如果要换成头像上传则设置成false
	//并且把avatar属性设置成true
	showFileList: {
		type: Boolean,
		default: true
	},
	//设置限制上传图片的大小
	size: {
		type: Number,
		default: 2
	},
	//上传时附带的额外字段,看你们后端是否要求,没有则不设置
	dataObj: {
		type: Object,
		default:()=> {
			return {type:5}//这个没有就默认空
		}
	},
	//上传的文件字段名,后端要求的字段名
	name: {
		type: String,
		default: 'files'
	},
	//文件列表的类型,text/picture/picture-card
	listType: {
		type: String,
		default: 'picture-card'
	},
},

这里属性定义完了,我们来重点说一下这个element上传的一个坑点。他的file-list也就是文件上传列表,将来我们用作多图照片墙回显的地方,需要按照它的格式来才行,否则你会发现,渲染不出来。好多的ui组件都是这个模式,移动端的vant组件,需要一个叫image:true的属性,一个叫url的属性。我们得element则是需要[{name: 'food.jpg', url: 'xxx.cdn.com/xxx.jpg'}]这… 所以我们在检测父组件传递过来的value时候,要把原来的['1','2','3']字符串list变成对象的list,即示例要求的图片数组形式。 还有就是我们在监测父组件传递过来的图片数据时,要把它传递过来的数据添加到上传图片成功后的通知变量-------emitList里面,这样我们才能在有父组件传递变量的情况下,我们再接着上传时不会清空或者覆盖父组件传递过来的数据。

//文件上传成功时的钩子
handleSuccess(file,fileInfo){
	//定义一个上传成功的变量,用来暂停监听父组件的数据,
	//因为我们还没有通知父组件接受参数
	this.changeFlag = false;
	this.loading = true;
	let item = file.data[0];
	//头像上传(不带预览)
	if(this.avatar){
		this.imageUrl = item;
		this.$emit('input', item);
	}else{
		//多图上传(带预览)
		this.emitList.push(item);
		let val = [...this.emitList];
		//如果需要回传的是字符串类型,则转字符串回传父组件
		if (this.isString) {
			val = val.join(',')
		}
		this.$emit('input', val);
	}
	this.loading = false;
},

//对父组件传递过来的数据进行监听
watch:{
	value:{
		immediate:true,   
		handler:function(val){
			//this.changeFlag利用这个变量,让我们在上传的时候暂停掉对父组件传递数据的监听
			//因为这个时候我们还没有通知父组件去接受我们上传成功后的图片路径
			if(this.changeFlag){
				//判断是否是头像上传,
				if(!this.avatar && val){
					//判断是否传递过来的数据是字符串,是字符串转为数组;有的业务需要如此
					//如果是字符串有可能为空
					if (this.isString) {
						val = val.split(',')
					}
					//把父组件传递过来的图片数组添加到上传成功后的暂存变量中,
					//再次上传后重新给父组件,更新图片绑定数据
					this.emitList = [...val];
					//循环遍历父组件数据,重新赋值成element组件所需要的回显数据
					val.forEach(item=>{
						let obj = {
							uid: item.uid,
							name: item.name,
							url: item
						}
						this.fileList.push(obj)
					})
					//如果是头像上传则直接赋值给图片路径即可
				}else{
					this.imageUrl = val;
				}
				
			}
			
		}
	},
},

基本核心代码全部写完,我们的上传回显就成功了。接下来我们再来完成删除和大图预览,我们的整个上传组件就完美写出来了

//我们先来写删除
//文件列表移除文件时的钩子
handleRemove(file, fileList) {
	//同样的暂停掉监听父组件数据,以免产生图片展示bug
	this.changeFlag = false;
	 //获取要删除的图片路径。判断是父组件传递的图片图片路径还是刚刚上传产生的图片路径
	const url = file.response ? file.response.data[0] : file.url;
	//遍历当前图片路径在通知给父组件的数据中是哪一个。然后删除掉。再通知父组件更新数据
	this.emitList.forEach((item, index) => {
		if (item.indexOf(url) > -1) {
			this.emitList.splice(index, 1)
			let val = [...this.emitList];
			//判断回传父组件是否是字符串类型
			if (this.isString) {
				val = val.join(',')
			}
			this.$emit('input', val);
		}
	})
},

我们打印一下删除时候的钩子参数,file和fileList,发现删除的时候,我们的父组件传递过来的图片儿数据和上传之后的图片数据,返回值是不太一致的 删除原有的父组件传递过来的图片时,file参数为:

image.png 如果是刚上传后再删除当前上传的图片,file参数为:

image.png 我们发现多了response参数,在这里我们才能取到图片的url,而不是下面最外层的blob本地url。 我们删除的基本思路是找到我们当前要删除的图片路径去对比我们要通知父组件更新的路径数据,两者的交集则是我们要在emitList中删除的。这样一个删除就完成了。接下来我们再来完成预览缩略图大图:

//点击文件列表中已上传的文件时的钩子
handlePreview(file) {
	if (this.showFileList && this.listType == 'picture-card') {
		this.imgSpreadUrl = file.url
		this.imgSpreadVisible = true
	}
},
//预览没什么好说的,打开dialog,然后给弹出层图片一个路径就好了,这个大家都懂,哈哈。

然后我们加上两个小判断,一个是应用头像模式,一个是回传字符串来满足服务端可能需要你的多图上传数据是字符串类型的要求。 最后给大家看一下效果图

image.png 好了,到这里我们的图片上传功能就做好了。最后给大家贴一组件代码和使用方式,希望对你有帮助的朋友能给点个赞,十分感谢!!!!!!!

//上传图片组件
<template>
  <div>
		<el-upload
			v-loading="loading"
		  class="upload-demo"
		  :action="action"
			:limit="limit"
			:show-file-list="showFileList"
			:file-list="fileList"
			:data="dataObj"
			:name="name"
			:headers="headers"
			:list-type="listType"
			:on-success="handleSuccess"
			:on-change="handleChange"
			:on-exceed="handleExceed"
		  :on-preview="handlePreview"
		  :on-remove="handleRemove"
			:before-upload="beforeUpload">
		  <div v-if="!avatar">
				<i slot="default" class="el-icon-plus"></i>
			</div>
			<div v-else>
				<img v-if="imageUrl" :src="imageUrl" class="avatar">
				<i v-else class="el-icon-plus avatar-uploader-icon"></i>
			</div>
		</el-upload>
		<el-dialog width="800px" :visible.sync="imgSpreadVisible" append-to-body>
		  <img class="spread-image" :src="imgSpreadUrl">
		</el-dialog>
	</div>
</template>

<script>
//引入token,一般上传图片接口可能需要token
import { getToken } from '@/utils/auth'
export default {
props:{
	value:{},
	//action是上传的地址
	action:{
		type:String,
		default: process.env.VUE_APP_BASE_API + process.env.VUE_APP_MY_UPLOAD_URL  || ''
	},
	//最大允许上传个数
	limit: {
		type: Number,
		default: 20
	},
	//是否采用头像上传模式
	avatar:{
		type:Boolean,
		default:false,
	},
	//根据业务需要,设置是否返回给父组件字符串类型
	//或者直接返回数组
	isString:{
		type:Boolean,
		default:false,
	},
	//是否显示已上传文件列表,如果要换成头像上传则设置成false
	//并且把avatar属性设置成true
	showFileList: {
		type: Boolean,
		default: true
	},
	//设置限制上传图片的大小
	size: {
		type: Number,
		default: 2
	},
	//上传时附带的额外字段,看你们后端是否要求,没有则不设置
	dataObj: {
		type: Object,
		default:()=> {
			return {type:5}//这个没有就默认空
		}
	},
	//上传的文件字段名,后端要求的字段名
	name: {
		type: String,
		default: 'files'
	},
	//文件列表的类型,text/picture/picture-card
	listType: {
		type: String,
		default: 'picture-card'
	},
},
	watch:{
		value:{
			immediate:true,   
			handler:function(val){
				//this.changeFlag利用这个变量,让我们在上传的时候暂停掉对父组件传递数据的监听
				//因为这个时候我们还没有通知父组件去接受我们上传成功后的图片路径
				if(this.changeFlag){
					//判断是否是头像上传,
					if(!this.avatar && val){
						//判断是否传递过来的数据是字符串,是字符串转为数组;有的业务需要如此
						//如果是字符串有可能为空
						if (this.isString) {
						  val = val.split(',')
						}
						//把父组件传递过来的图片数组添加到上传成功后的暂存变量中,
						//再次上传后重新给父组件,更新图片绑定数据
						this.emitList = [...val];
						//循环遍历父组件数据,重新赋值成element组件所需要的回显数据
						val.forEach(item=>{
							let obj = {
								uid: item.uid,
								name: item.name,
								url: item
							}
							this.fileList.push(obj)
						})
						//如果是头像上传则直接赋值给图片路径即可
					}else{
						this.imageUrl = val;
					}
					
				}
				
			}
		},
	},
	computed: {
	  // 设置上传的请求头部
	  headers() {
	    return {
	      'ContentType': 'multipart/form-data',// 设置Content-Type类型为multipart/form-data
	      'token': getToken()// 设置token
	    }
	  }
	},
  data() {
    return {
			fileList:[],//element自带的图片list
			emitList:[],//回显的图片list
			imgSpreadVisible:false,//查看缩略图的弹窗状态
			imgSpreadUrl:'',//缩略图大图图片路径
			loading:false,//加载状态
			changeFlag:true,
			imageUrl:''
    }
  },
  methods: {
		//上传前校验一下图片大小
		beforeUpload(file){
			const isLtSize = file.size / 1024 / 1024 < this.size
			if (!isLtSize) {
			  this.$message.error('文件大小不能超过 ' + this.size + 'M')
			}
			return isLtSize
		},
		//文件上传成功时的钩子
		handleSuccess(file,fileInfo){
			//定义一个上传成功的变量,用来暂停监听父组件的数据,
			//因为我们还没有通知父组件接受参数
			this.changeFlag = false;
			this.loading = true;
			let item = file.data[0];
			//头像上传(不带预览)
			if(this.avatar){
				this.imageUrl = item;
				this.$emit('input', item);
			}else{
				//多图上传(带预览)
				this.emitList.push(item);
				let val = [...this.emitList];
				//如果需要回传的是字符串类型,则转字符串回传父组件
				if (this.isString) {
				  val = val.join(',')
				}
				this.$emit('input', val);
			}
			this.loading = false;
		},
		//文件状态改变时的钩子
		handleChange(){
			if (this.loading) {
				//用于图片校验
			  this.$emit('change', true)
			}
		},
		//文件列表移除文件时的钩子
		handleRemove(file, fileList) {
			//同样的暂停掉监听父组件数据,以免产生图片展示bug
			this.changeFlag = false;
			 //获取要删除的图片路径。判断是父组件传递的图片图片路径还是刚刚上传产生的图片路径
			const url = file.response ? file.response.data[0] : file.url;
			//遍历当前图片路径在通知给父组件的数据中是哪一个。然后删除掉。再通知父组件更新数据
			this.emitList.forEach((item, index) => {
			  if (item.indexOf(url) > -1) {
			    this.emitList.splice(index, 1)
			    let val = [...this.emitList];
					//判断回传父组件是否是字符串类型
					if (this.isString) {
					  val = val.join(',')
					}
			    this.$emit('input', val);
			  }
			})
		},
		//点击文件列表中已上传的文件时的钩子
		handlePreview(file) {
			if (this.showFileList && this.listType == 'picture-card') {
				this.imgSpreadUrl = file.url
				this.imgSpreadVisible = true
			}
		},
		//文件超出个数限制时的钩子
		handleExceed(files, fileList) {
			this.$message.error('最多上传' + this.limit + '个文件')
		},
  }
}
</script>

<style scoped>
	.spread-image{display: block;max-width: 100%;max-height: 500px;margin: auto;}
	.avatar{width: 100%;height: auto;}
</style>

//页面使用
<template>
  <div class="container">
		<!-- 数组类型的多图上传 -->
		<upload-image v-model="arrayImage"></upload-image>
		<!-- 字符串类型的多图上传 -->
		<upload-image v-model="stringImage" :isString="true" ></upload-image>
		<!-- 头像类型的上传 -->
		<upload-image v-model="avatarValue":avatar="true" :showFileList="false"></upload-image>
	</div>
</template>

<script>
import uploadImage from './components/upload.vue'
export default {
	components:{
		uploadImage
	},
  data() {
    return {
	  arrayImage:[],//数组类型的多图上传
      stringImage:'https://gcjf.guochengjinfu.cn/20201112/textProPic/1e6a8d9a94504af2b2f0d0136987de31.png',
      //字符串类型多图上传
	  avatarValue:'',//头像上传
    }
  }
}
</script>

希望对朋友们有帮助,最后的最后~~~~~~~~~给点个赞吧!谢谢!