vue组件之高内聚、低耦合(一次组件自定义v-model的封装经历)
前段时间公司开通了积分机制,关乎到升级大计。看着自己博客里的两篇随笔,我哭了。三年了。。只写了两篇博客。哎,平常实在是不想写,甚至连引用别人的文章都不愿意。现如今没办法了,写吧。
本来想写一个关于vue插槽和动态组件的博客着。写了一个星期了,还没写完(上班没时间,下班不想动)。前两天新调到人资写H5遇到了一个组件传值的小问题。没改动,还是让前辈给改的。赶脚脸很疼。先看一下这个项目的交互图。
可以看到的是这里有一个选择组件,有单选和多选的功能。通过点击触发选项变化,单选的话点击标签如果已经选中没变化,如果没选中,状态改为选中,其他已选中的取消选中;多选的话点击不会对其他选项造成影响,只对自己的状态进行取反操作。下面是组件封装之前的代码
1 /** queryTab组件*/ 2 3 <template> 4 <div id="query_tab" @click.stop :class="activeValue == 'postIdList' ? 'query_tab_padding': ''"> 5 <div class="queyr_tab_title"> 6 {{title}} 7 <span 8 v-if="title=='岗位'||title=='状态'" 9 class='able-more-query'>(可多选) 10 </span> 11 </div> 12 <van-row gutter="20" class='query_tabs_btn'> 13 <van-col span="8" 14 v-for="(item,index) in tabs" 15 :key="index"> 16 <van-button 17 @click="selsctQuery(item)" 18 :class="active(item) ? 'active':''" 19 size="small" 20 type="text"> 21 {{item.dictValue}} 22 </van-button> 23 </van-col> 24 </van-row> 25 <slot></slot> 26 </div> 27 </template> 28 <script> 29 export default { 30 props:{ 31 // 字典名称 32 title:{ 33 type:String, 34 default:'' 35 }, 36 // 字典列表 37 tabs:{ 38 type:Array, 39 default: ()=>[] 40 }, 41 // 多选 42 multi:{ 43 type:Boolean, 44 default:false 45 }, 46 // 当前选中 47 activeValue: { 48 type:String, 49 default:'' 50 }, 51 // 表单 52 form:{ 53 type:Object, 54 default:()=>({}) 55 }, 56 // 对比字段 57 valueKey: { 58 type:[Array,String], 59 default:"dictId" 60 } 61 }, 62 computed:{ 63 /** @information 当前高亮 */ 64 active() { 65 let {form,multi,activeValue,tabs} = this; 66 return data=>{ 67 if(!multi){ 68 // 单选判断选中的那个 69 return form[activeValue] === data.dictId; 70 }else { 71 // 多选 72 let index = form[activeValue].findIndex(item=>item === data.dictId); 73 return index !== -1; 74 } 75 } 76 } 77 }, 78 methods:{ 79 /** @information 选择查询条件 */ 80 selsctQuery({dictId}){ 81 let { multi,form,activeValue:key } = this 82 let current = dictId 83 if(multi){ 84 let value = form[key]?[...form[key]]:[] 85 let i = form[key].findIndex(el => el===dictId) 86 if(i===-1){ 87 value.push(dictId) 88 }else{ 89 value.splice(i,1) 90 } 91 current=value 92 } 93 this.$emit('selectQuery',{dictId:current,key}) 94 } 95 } 96 }
从代码可以看出,它接收了 共6个变量
字典名称 title,
字典列表 tabs,
是否多选 multi,
当前选择器对应form表单的字段 activeValue,
查询表单对象 form,
对比字段 valueKey ,(这个字段我估计之前设计的应该是字典名称和字典值的字符串描述或者数组描述,从 default:"dictId" 看出来的,不过后来没有用。)
1个计算属性方法遍历更新标签点击(高亮)状态,
和一个标签点击事件 selsctQuery根据是单选还是多选判断去判断current的值,然后将结果通过$emit 传给外部selectQuery事件,那么我们在看看这个组件在页面中是怎么应用的。
1 /** queryTab组件的调用*/ 2 <template> 3 . 4 . 5 . 6 <QueryTab 7 :title="selectQueryTab.label" 8 v-if="selectQueryTab.value !== 'other' && selectQueryTab.value" 9 :tabs="tabDict" 10 :activeValue='selectQueryTab.value' 11 :form='form' 12 @selectQuery='selectQuery' 13 :multi="selectQueryTab['multi']"> 14 <div :class="[selectQueryTab.value == 'postIdList' ? 'btn_abs': 'bottom_btn']" v-if="selectQueryTab.value !== 'source'"> 15 <van-button type="default" @click="resetCurrent">重置</van-button> 16 <van-button type="primary" @click="query">查询</van-button> 17 </div> 18 </QueryTab> 19 <OtherQuery v-if="selectQueryTab.value == 'other'" 20 :form='form' 21 :salaryDictId="salaryDictId" 22 @selectOtherQuery='selectOtherQuery' 23 @reset='resetOther' 24 @query='query' 25 @setSalaryDict="salaryDictId = $event" 26 @changePicker='changePicker'> 27 </OtherQuery> 28 . 29 . 30 . 31 32 </template> 33 <script> 34 export default { 35 data() { 36 return { 37 form:{ 38 postIdList:[], // 岗位 39 memberStatus:[], // 状态 40 source:'', // 来源 41 /** 只有以上三项是外边的,其他都是更多里的*/ 42 memberType:'', // 类型 43 memberSex:'', // 性别 44 education:'', // 简历学历 45 memberId:'', // 人员id 46 type:'', // 采集类型 47 collectionTime:[], // 采集时间 48 collectionEducation:"", // 采集学历 49 age:[], // 年龄 50 recruitmentChannel: '', //招聘渠道 51 registrationTime:[], // 注册时间 52 expectationPlace:[], // 期望工作地点 53 expectationPost:"", // 期望职位 54 expectationSalary:"", // 期望薪资 55 salaryDictId:null, 56 isdelete:"", // 删除状态 57 workExperience:"", // 工作经历 58 schoolName:"" // 学校名称 59 }, 60 otherKey: [ 61 'memberSex', 62 'age', 63 'collectionTime', 64 'registrationTime', 65 'expectationPlace', 66 'expectationSalary', 67 "expectationPost", 68 "collectionEducation", 69 "education", 70 "workExperience", 71 "schoolName", 72 "type", 73 "isdelete", 74 "recruitmentChannel"], // 更多选项的key 75 } 76 }, 77 78 methods:{ 79 /** 选择查询条件 */ 80 selectQuery({dictId:val,key}) { 81 let {multi,value} = this.selectQueryTab; 82 // 判断是单选多选 83 if(!multi){ 84 // 单选替换 85 this.form[key] = this.form[key] === val ? '' : val; 86 value == 'source' && (this.show = false) 87 this.queryList(); 88 }else { 89 this.form[key] = val; 90 } 91 } 92 /** 选择其他 */ 93 selectOtherQuery({dictId,key}) { 94 this.form[key] = dictId; 95 } 96 } 97 } 98 </script>
在外边调用queryTab组件就是把该传的值传进去然后用selectQuery事件接收值变化然后改变form的属性。(这里没有传valueKey,我最后发现是在请求字典的接口里统一做了处理,转成dictId、dictValue)。
单看的话,这么写也没什么。其实我以前在项目里也是这么干的。可是这里有一个问题就是他有3个(岗位、状态、来源)在外边一层,而更多地值(更多里的)却在下一层组件里如果都放到一个组件太乱,如果将组件分开。就会有子组件传值给父组件,然后父组件再传值给爷爷组件的麻烦,而且还是每一个值改变一次,就传一次。再来看一下otherQuery里面的代码
1 /** otherQuery组件*/ 2 <template> 3 <div @click.stop id="person_manage_other_query" :style="'maxHeight:'+height+'px'"> 4 <!-- 性别 --> 5 <QueryTab 6 title="性别" 7 :tabs='sexDictionary' 8 :form='form' 9 activeValue='memberSex' 10 @selectQuery="$emit('selectOtherQuery',$event)"> 11 </QueryTab> 12 <!-- 期望地点 --> 13 <QueryTab 14 title="期望地点" 15 :tabs='expectationPlace' 16 :form='form' 17 :multi="true" 18 activeValue='expectationPlace' 19 @selectQuery="$emit('selectOtherQuery',$event)"> 20 </QueryTab> 21 . 22 . 23 . 24 <div class="bottom_btn"> 25 <van-button type="default" @click="$emit('reset')">重置</van-button> 26 <van-button type="primary" @click="$emit('query')">查询</van-button> 27 </div> 28 </div> 29 </template> 30 <script> 31 export default { 32 props:{ 33 form:{ 34 type:Object, 35 default:()=>({}) 36 } 37 }, 38 } 39 </script>
好像otherQuery组件没有干什么事情只是通过下面这句代码将值再次传给了外边去处理。
1 @selectQuery="$emit('selectOtherQuery',$event)"
好吧,现在看来组件这么写也没什么太大的问题,用起来也不错,最少现在一直健壮的运行着。我就是赶脚在改bug的时候要看好多的地方以为关联性太强了。而且值改变还要去触发外边改变值,最少在我刚接这个项目的时候我改起来不是很顺手,里边外边都需要关注。(嗯,我就是在找理由,要不我的博客写什么呀。为了积分只能说这么用不好)还是将组件重新封装一下,进行一下解耦才对得起‘高内聚,低耦合’这句话。
那么有没有一个方法让组件之间的关联性不这么紧密呢,这个时候我想起了vue的v-model我把组件进行了一把封装,下面看一下代码
1 /**新queryTab组件*/ 2 <template> 3 <div id="query_tab" @click.stop > 4 <div class="queyr_tab_title"> 5 {{title}} 6 <span v-if="mult" class='able-more-query'>(可多选) </span> 7 </div> 8 <van-row gutter="20" class='query_tabs_btn'> 9 <van-col span="8" 10 v-for="(item,index) in tabs" 11 :key="index"> 12 <van-button 13 @click="selectQuery(index)" 14 :class="active(item) ? 'active' : ''" 15 size="small" 16 type="text"> 17 {{item[lable]}} 18 </van-button> 19 </van-col> 20 </van-row> 21 <slot></slot> 22 </div> 23 </template> 24 <script> 25 export default { 26 name: 'QueryTabCom', 27 model: { 28 prop: 'select', 29 event: 'up' 30 }, 31 props:{ 32 // 字典名称 33 title:{ type: String, default: '' }, 34 // 字典列表 35 tabs:{ type:Array, default: () => ([]) }, 36 // 多选 37 mult:{ type:Boolean, default: false }, 38 // 显示标签 39 lable: {type: String, default: 'dictName'}, 40 // 标签对应值 41 value: {type: String, default: 'dictId'}, 42 // model绑定值再组件内的引用 43 select: {type: [String , Number , Array], default: ''} 44 }, 45 46 data() { 47 return { 48 // 当前选中列表 49 actives: {}, 50 } 51 }, 52 methods: { 53 /**标签点击事件 */ 54 selectQuery(ind) { 55 let {actives, tabs, value, mult} = this 56 let select= null 57 if(!mult) { 58 //如果是单选点击的是已选的选项就清空,不是已经选择的选项就切换 59 select = tabs[ind].dictId === this.select ? null : tabs[ind].dictId 60 }else{ 61 //如果是多选的话现将选项本身取反然后过滤返回正确的列表 62 actives[ind] = !actives[ind] 63 let list = Object.keys(actives).filter( key => actives[key]) 64 select = list.map( ind => tabs[ind][value]) 65 } 66 //更新值 67 this.$emit('up', select) 68 } 69 }, 70 computed: { 71 /** 计算显示高亮状态*/ 72 active() { 73 let { mult, tabs, select } = this; 74 return obj => { 75 if(!mult){ 76 // 单选判断选中的那个 77 return (select || select === 0 || select === '0') && select.toString() === obj.dictId.toString() ; 78 }else { 79 // 多选 80 let has = select.find( item => item.toString() === obj.dictId.toString()); 81 return Boolean(has || has===0); 82 } 83 } 84 } 85 } 86 } 87 </script>
好像和原来的组件也没什么区别是吧,还是有点区别的。看这段代码
1 model: { 2 prop: 'select', 3 event: 'up' 4 },
这个是在v2.2.0新增的一个特性
一个组件上的 v-model 默认会利用名为 value 的 prop 和名为 input 的事件,但是像我的自定义组件就用到了value属性,而且组件里面也没有input事件,幸好现在vue就有了这么一个新的东东专门用来做自定组件的v-model。其中的prop就是你自定义组件v-model绑定的值在组件内部的引用(可以是任何类型,但是在这里光声明是不够的,还要再porps里边接收一下),event就是组件同步数据的时候要emit的事件名称。这样vulue属性和input事件就解放出来了(以前做自定义的v-model还要考虑value属性的冲突问题,现在model 选项可以用来避免这样的冲突),这里我props里边多接收了两个变量lable,和value分别对应字典的key和value并且设置了默认值。
下面再看看外边引用这个组件的时候的新代码是什么样子的。
1 <template> 2 . 3 . 4 . 5 <QueryTab 6 :title="selectQueryTab.label" 7 v-if="selectQueryTab.value !== 'other' && selectQueryTab.value" 8 :tabs="tabDict" 9 v-model='form[selectQueryTab.value]' 10 lable='dictValue' 11 value='dictId' 12 :multi="selectQueryTab['multi']"> 13 <div :class="[selectQueryTab.value == 'postIdList' ? 'btn_abs': 'bottom_btn']" v-if="selectQueryTab.value !== 'source'"> 14 <van-button type="default" @click="resetCurrent">重置</van-button> 15 <van-button type="primary" @click="query">查询</van-button> 16 </div> 17 </QueryTab> 18 <OtherQuery v-if="selectQueryTab.value == 'other'" 19 :form='form' 20 :salaryDictId="salaryDictId" 21 @selectOtherQuery='selectOtherQuery' 22 @reset='resetOther' 23 @query='query' 24 @setSalaryDict="salaryDictId = $event" 25 @changePicker='changePicker'> 26 </OtherQuery> 27 . 28 . 29 . 30 31 </template> 32 <script> 33 export default { 34 data() { 35 return { 36 form:{ 37 postIdList:[], // 岗位 38 memberStatus:[], // 状态 39 source:'', // 来源 40 /** 只有以上三项是外边的,其他都是更多里的*/ 41 memberType:'', // 类型 42 memberSex:'', // 性别 43 education:'', // 简历学历 44 memberId:'', // 人员id 45 type:'', // 采集类型 46 collectionTime:[], // 采集时间 47 collectionEducation:"", // 采集学历 48 age:[], // 年龄 49 recruitmentChannel: '', //招聘渠道 50 registrationTime:[], // 注册时间 51 expectationPlace:[], // 期望工作地点 52 expectationPost:"", // 期望职位 53 expectationSalary:"", // 期望薪资 54 salaryDictId:null, 55 isdelete:"", // 删除状态 56 workExperience:"", // 工作经历 57 schoolName:"" // 学校名称 58 }, 59 otherKey: [ 60 'memberSex', 61 'age', 62 'collectionTime', 63 'registrationTime', 64 'expectationPlace', 65 'expectationSalary', 66 "expectationPost", 67 "collectionEducation", 68 "education", 69 "workExperience", 70 "schoolName", 71 "type", 72 "isdelete", 73 "recruitmentChannel"], // 更多选项的key 74 } 75 }, 76 } 77 </script>
是不是感觉好多了,没有了只改变之后的事件了,因为通过v-model绑定了吗。
这里再提一个新的问题如果某一个单选属性后端要的是数组格式,而其他的单选属性要的是字符串格式怎么办。这里我又画蛇添足的加入了一个type属性用来接收组件希望的绑定值类型。
看代码。
1 <template> 2 <div id="query_tab" @click.stop > 3 <div class="queyr_tab_title"> 4 {{title}} 5 <span v-if="mult" class='able-more-query'>(可多选) </span> 6 </div> 7 <van-row gutter="20" class='query_tabs_btn'> 8 <van-col span="8" 9 v-for="(item,index) in tabs" 10 :key="index"> 11 <van-button 12 @click="selectQuery(index)" 13 :class="active(item) ? 'active' : ''" 14 size="small" 15 type="text"> 16 {{item[lable]}} 17 </van-button> 18 </van-col> 19 </van-row> 20 <slot></slot> 21 </div> 22 </template> 23 <script> 24 export default { 25 name: 'QueryTabCom', 26 model: { 27 prop: 'select', 28 event: 'up' 29 }, 30 props:{ 31 // 字典名称 32 title:{ type: String, default: '' }, 33 // 字典列表 34 tabs:{ type:Array, default: () => ([]) }, 35 // 多选 36 mult:{ type:Boolean, default: false }, 37 // 显示标签 38 lable: {type: String, default: 'dictName'}, 39 // 标签对应值 40 value: {type: String, default: 'dictId'}, 41 // model绑定值再组件内的引用 42 select: {type: [String , Number , Array], default: ''}, 43 // model希望绑定值类型 44 type: {type: String, default: 'string'} 45 46 }, 47 48 data() { 49 return { 50 // 当前选中列表 51 actives: {}, 52 } 53 }, 54 methods: { 55 /**标签点击事件 */ 56 selectQuery(ind) { 57 let { actives, tabs, value, mult, type } = this 58 let select= null 59 if(!mult) { 60 //如果是单选点击的是已选的选项就清空,不是已经选择的选项就切换 61 select = tabs[ind].dictId === this.select ? null : tabs[ind].dictId 62 select = type === 'string' ? select.toString : [select] 63 }else{ 64 //如果是多选的话现将选项本身取反然后过滤返回正确的列表 65 actives[ind] = !actives[ind] 66 let list = Object.keys(actives).filter( key => actives[key]) 67 select = list.map( ind => tabs[ind][value]) 68 select = type === 'string' ? select.join(',') : select 69 } 70 //更新值 71 this.$emit('up', select) 72 } 73 }, 74 computed: { 75 /** 计算显示高亮状态*/ 76 active() { 77 let { mult, tabs, select } = this; 78 return obj => { 79 if(!mult){ 80 // 单选判断选中的那个 81 return (select || select === 0 || select === '0') && select.toString() === obj.dictId.toString() ; 82 }else { 83 // 多选 84 let has = select.find( item => item.toString() === obj.dictId.toString()); 85 return Boolean(has || has===0); 86 } 87 } 88 } 89 } 90 } 91 </script>
当然了,目前只有数组和字符串两种,为什么没有number呢(这里不得不吐槽一下,按说吧id这个东西作为键值为了加快搜索在数据库里边就都用自增int的,可是在咱们这里我愣是遇到了带字母组合的id这种奇葩的情况,所以为了不再字符转数字的时候出现NaN我只能放弃number这个类型了)。
到这个时候是不是应该有掌声了,本来我也以为大功告成了。可是现实给了我一个大嘴巴,好疼好疼的那种。看咱们最开始的交互图,这里不是有一个来源吗,它是单选,他触发查询是即时的,在只改变的时候就触发了,因为没有查询。在源代码里是这样处理的。
1 selectQuery({dictId:val,key}) { 2 let {multi,value} = this.selectQueryTab; 3 // 判断是单选多选 4 if(!multi){ 5 // 单选替换 6 this.form[key] = this.form[key] === val ? '' : val; 7 value == 'source' && (this.show = false) 8 this.queryList(); 9 }else { 10 this.form[key] = val; 11 } 12 }
也就是说每一次是单选的时候就触发查询(不得不是真的很鬼的,因为在外部只有来源一个 单选,而otherQuery组件的事件又是通过selectOtherQuery方法做的更新)可是我总不能 在来源改变的时候单独触发一个事件来做查询吧,那样就有违我重新封装这个组件的初衷了。而 在外边又是所有属性都放到form这个对象里边了,就算是深度监听也只是能知道form里有属性 改变,而不知道具体哪个属性改变。问了一下度娘,解决方式还挺多。
一个是computer加watch就是声明一个计算属性返回‘来源’这个属性,然后就可以监控新 计算的这个属性区出发查询事件了。还是贴一下代码吧。
1 computed: { 2 source(){ 3 return this.form.source 4 } 5 }, 6 watch: { 7 source(n,o){ 8 this.queryList() 9 } 10 }
另一个就是纯利用计算属性,代码是这样的
1 computed: { 2 source(){ 3 this.queryList() 4 return this.form.source 5 } 6 }
我用的是前者,如果是用后一种方法的话还要在页面加一个dom节点的引用还要隐藏,因为计算出来的属性并没有用,只是用来监测值的改变用的。
1 <div v-show = 'false'>{{selectSource}}</div>
当然实时监测对象属性的方法还有很多,我只是随便挑了两个演示一把,毕竟这个不是今天的主题。
至此我的改造都弄完了,自测了一把,很顺滑。赶脚么么哒。写博客没经验,文中如有不对的地方,欢迎大家指出。
个人邮箱18231590815@163.com
本文来自博客园,作者:hauner,转载请注明原文链接:https://www.cnblogs.com/hauner/p/13692021.html