二次封装,是指因为业务上的需要,对element-ui、antd、e-chart等其他组件库,做二次封装。以下是常见的二次封装场景:
说个我做项目遇到的情况,项目里用到组件库的是antd,antd的搜索框长这样
但是ui图长这样:
如果要实现和ui图一样的效果,你就需要利用前缀和后缀的插槽,加上搜索图标和清空图标:
<a-input :placeholder="placeholder" :style="inputStyle" @keydown.enter=handleEnter v-model="searchValue"
@change="searchValueChange">
<a-icon slot="prefix" type="search"/>
<a-icon slot="suffix" v-show="searchValue!='' || manualHide" type="close" style="cursor: pointer"
@click="clearSearchValue"/>
a-input>
有时还需要改改样式:
那么这样就带来一个问题,要知道搜索框可是复用频率很高的,如果每次都需要粘贴同样的插槽和样式代码,那不是很憨憨吗?
而且后期维护,如果要改搜索框的样式,那就得改好几处地方,所以这一段组件是一定要封装的。
在真实的项目中,我们需要封装一些与业务数据绑定的选择器。
比如下面这个表格选择器,在文件夹里面选择用户上传的表。基本项目所有业务都是需要选表的,所以这个组件复用频率很高。
这种业务组件的特点是,组件会带有很多业务数据请求的方法,比如下面这种请求后台数据,然后把数据绑定在表格组件上。
loadData(arg) {
let params = this.getQueryParams()//查询条件
this.loading = true
queryFolderList(params).then((res) => {
if (!res.success) {
this.$notify['error'].call(this, {
key: 'ErrorNotificationKey',
message: '操作信息提示',
description: res.message
})
return
}
this.dataSource = res.list
}).finally(() => {
this.loading = false
})
}
,
这种业务组件是需要封装的,因为如果不封装,每次遇到同样的场景,就要把这些数据接口请求再粘贴一次,这种操作属实有点憨憨,所以一定要封装。
有时候我们的组件还需要拓展功能,这种场景也是需要封装的,比如antd表格的基础表格是没有筛选的,
要实现列筛选功能,是需要我们自己手写逻辑的,那么我们就应该把这段补充的逻辑封装在组件里。
如果做二次封装,没有暴露组件库本身的属性和方法。那说实话,你这不叫封装组件,你这叫“坐牢”,只有封没有装那能叫封装吗?
还是以封装antd的输入框为例,我只是对样式做了一些更改,那么antd输入框中的props、事件、插槽和方法,是应该暴露出来给父组件的,否则封装的组件压根就没法用。
那我这里怎么绑定呢?不可能一个个照着文档把props全绑上去吧?
//憨憨的做法:照着文档一个个地绑定props
props:{
placeholder: {
type: String,
default: ''
},
defaultValue: {
type: String,
default: ''
},
//把所有的props一个个填上去......
}
真当程序员是码农啊?别干这些“重复”的体力活了,具体怎么做呢?这里讲一下封装技巧:
实现思路:用计算属性获取this.$attrs的值,然后用v-bind绑定,下面antd的弹窗组件为例:
HTML
<a-modal v-bind="_attrs">
将一些未处理的参数或特殊处理的参数绑定到 a-modal 上
computed: {
_attrs() {
let attrs = { ...this.$attrs }
return attrs
},
}
<a-modal
@ok="handleOk"
@cancel="handleCancel"
>
其实这里我们需要抛change等事件给父组件,如果一个一个事件的写emit太憨了,有十个emit难道手动写十次emit吗?
下面推荐一个简单的方法:v-on="$listeners"
这段代码可以动态捕捉到emit事件
<a-modal
v-on="$listeners"
>
这样就不用手动再抛事件,给a-modal的组件了。
但是在开发中,我们有时需要覆写组件的@change等方法,那么这里需要就注意一个问题:
v-on=“$listeners“与重写事件有冲突,会执行两次。
说直白点就是,如果你手动写了v-on=“$listeners”,然后你又自己重写了其中的事件,那么这个事件就会执行两次。
比如 element 组件 el-select,change 事件重写,返回更多的参数:
xxx
method: {
changeSelect(val) {
this.$emit('change', val)
}
}
使用:
结果: 执行2次 , 多一个 undefined
第一个是v-on=“$listeners”,第二个是@change="onSelectChange
解决:
去掉 @change=“changeSelect”, 使用 new$listeners 继承原有属性,并覆盖原有事件
xxx
computed: {
new$listeners() {
return Object.assign(this.$listeners, {
//在这里覆盖原有的change事件
change: this.changeSelect
})
}
},
method: {
changeSelect(val) {
//你自己的业务逻辑
//...
this.$emit('change', val)
}
}
同样的道理,插槽也是必须要暴露给父组件的,主要靠Vue提供的 s l o t s 和 slots和 slots和scopedSlots,具体方法如下:
<a-modal>
<slot>slot>
<template v-for="slotName of scopedSlotsKeys" :slot="slotName">
<slot :name="slotName">slot>
template>
<template v-for="slotName of slotsKeys" v-slot:[slotName]>
<slot :name="slotName">slot>
template>
a-modal>
计算属性拿到父组件注册的插槽:
computed: {
slotsKeys() {
return Object.keys(this.$slots).filter((key) => !this.usedSlots.includes(key))
},
scopedSlotsKeys() {
return Object.keys(this.$scopedSlots).filter((key) => !this.usedSlots.includes(key))
},
},
拿element-ui的 「Cascader级联选择器」 来说吧,我有一个需求,需要用到getCheckedNodes()这个方法
这里是我的父组件要调用这个方法,如果手动的注册事件,一层层的传递也挺麻烦。
//二次封装组件
methods:{
getCheckedNodes(){
return this.$refs.cascader.getCheckedNodes()
}
}
toggleDropDownVisible(visible){
return this.$refs.cascader.toggleDropDownVisible(visible)
}
}
//...
}
具体调用方法如下,二次封装的组件暴露出实例,利用实例再调用element内部的方法:
//二次封装组件
methods:{
getCascader(){
return this.$refs.cascader
}
}
}
父组件要调用实例方法,可以先获取实例,再调用方法(即链式调用):
//引入上面封装组件的父组件
methods:{
loadData(){
this.$nextTick(() => {
this.$refs.child.getCascader().getCheckedNodes()
this.$refs.child.getCascader().toggleDropDownVisible(true)
//只要拿到这个实例,this.$refs.child.getCascader(),想调方法后面跟链式调用即可
})
}
}
}
讲完了具体的方法,我们再来讲一些封装的原则,遵守这些规范,你能少加很多班~
PS:别问我是怎么总结出来的,都是实战中的血与泪╮(╯﹏╰)╭
慎重封装跟业务有关的代码,你可以根据一个原则来判断,如果业务接口更新的是props
中的数据,那就不要这段业务代码,放到父组件去做更好。因为这样会破坏vue的单向数据流,带来维护上的困难。
举个我自己遇到的坑,下面这个表格组件,写了一个业务接口,是更新表格的dataSource。
methods:{
loadData(){
getData().then((res) => {
this.dataSource = res.simpleDataList
})
}
}
}
而这个属性是实际上是antd表格组件的props
。
我这里犯得错误是,我没有把它定义为props
,而是放在了data
里:
data() {
return {
dataSource:[]
}
}
这就违背我上面说的第一条原则,一定要暴露封装组件的所有props
,所以这个属性不应该是data
,而是props
。而我这里更新了本该是props的数据,这就是组件封装上很严重的问题,我的父组件想改变dataSource变得很困难,这给我后面的维护带来了无尽的折磨…
不要修改props
的数据(这样写控制台会报错)
更不要修改本该是props的数据。
不要在一个vue文件内,封装多个数据的入口处理,你应该遵守“单一职责”。
这次的反面教材,还是上面说的数据表组件。
由于加载数据表有很多个接口,每个接口的参数又不一样,当时就把很多个接口分别做成多个入口方法,但这样后期维护很难受,因为这个接口是应该我父组件去请求的,每次后台更新接口参数,放在子组件我就很难维护,因为跟业务耦合的太死了。
所以后面这个组件我直接重写了,定位为纯展示的组件,只加了样式的优化,还有添加筛选功能。所有的数据,都由父组件来传props控制。
所以,当你发现一个组件要耦合多个数据入口时,就不要把请求写在子组件里面,而是把这个组件改造为纯展示组件,跟业务解耦,把业务请求交给父组件。
要不然随着后期业务的变化,你的维护成本会变得越来越高。
内容和载体要分两个vue文件,内容是页面的主要内容,载体是展示的方式,例如弹窗、抽屉等等。
比如说一个表单填写的弹窗的页面,应该分为:表单vue文件,和弹窗vue文件两种。
还是以代码为例:
<bar-title-modal :title="title" :width="width" :visible="visible"@ok="handleOk" >
<a-form :form="form">
<a-form-item label="用户选择" style="width: 500px">
<j-select-multi-user v-decorator="['users']"/>
{{ getFormFieldValue('users') }}
a-form-item>
<a-form-item label="用户选择" style="width: 500px">
<j-select-multi-user v-model="users" >j-select-multi-user>
{{ users }}
a-form-item>
a-form >
bar-title-modal>
正确的做法是把是表单内容单独封装为data-form组件,而弹窗组件作为载体的组件,
<bar-title-modal :title="title" :width="width" :visible="visible"@ok="handleOk" >
<data-form ref="realForm">data-form>
bar-title-modal>
这样就能保证表单内容组件的复用。比如你下一次不需要在弹窗里放表单了,而是直接在页面上展示,或者换成抽屉其他载体,你就不需要单独把表单这段代码复制来复制去了。
遵守载体分离原则,解耦内容和载体,就能方便后期的拓展开发。
我们封装输入框、复选框这种组件的时候,还需要保留他们的v-model
,这样父组件在调用的时候也能保持数据的双向绑定,使用得更加丝滑!
以封装antd的单选框、复选框等类型的输入控件
<a-radio-group v-model="value" @change="onChange">
<a-radio :value="1">
A
a-radio>
<a-radio :value="2">
B
a-radio>
<a-radio :value="3">
C
a-radio>
a-radio-group>
接下来我们写好model、props和change事件即可
{
model: {
prop: 'value',
event: 'change'
},
props: {
value: String
},
methods:{
onChange(value):{
this.$emit('change',e.target.value);
}
}
}
现在在这个组件上使用v-model
的时候:
<base-checkbox v-model="myValue">base-checkbox>
这里的 myValue 的值将会传入这个名为 value
的 prop。
同时当 触发一个 change
事件并附带一个新的值的时候,这个 myValue
的 property 将会被更新。
注意:你仍然需要在组件的 props
选项里声明 value
这个 prop。
当修改单个文件的样式时,以 less
为例,如果你想要修改组件的样式,可以使用 /deep/ 或 >>> 来深度选择到你要修改的样式(这能够帮你省去一大串的类名)。
.dialog-wrapper {
/deep/.el-dialog__body{
border: solid 1px #999;
}
}
如果你要修改全局的样式,第一种方法,你可以在全局样式文件中写样式覆盖,引入到 main.js 中即可全局生效。如下:
import "./assets/css/index.css";
参考文章:
https://www.h5w3.com/112064.html