学会正确地二次封装组件,让同事抱着你的大腿喊大神!


highlight: a11y-dark

一、二次封装的需求场景

二次封装,是指因为业务上的需要,对element-ui、antd、e-chart等其他组件库,做二次封装。以下是常见的二次封装场景:

1.封装UI样式

说个我做项目遇到的情况,项目里用到组件库的是antd,antd的搜索框长这样
学会正确地二次封装组件,让同事抱着你的大腿喊大神!_第1张图片

但是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>

有时还需要改改样式:


那么这样就带来一个问题,要知道搜索框可是复用频率很高的,如果每次都需要粘贴同样的插槽和样式代码,那不是很憨憨吗?

而且后期维护,如果要改搜索框的样式,那就得改好几处地方,所以这一段组件是一定要封装的。

2.封装业务逻辑

在真实的项目中,我们需要封装一些与业务数据绑定的选择器。
比如下面这个表格选择器,在文件夹里面选择用户上传的表。基本项目所有业务都是需要选表的,所以这个组件复用频率很高。
学会正确地二次封装组件,让同事抱着你的大腿喊大神!_第2张图片
这种业务组件的特点是,组件会带有很多业务数据请求的方法,比如下面这种请求后台数据,然后把数据绑定在表格组件上。

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
        })
      }
      , 

这种业务组件是需要封装的,因为如果不封装,每次遇到同样的场景,就要把这些数据接口请求再粘贴一次,这种操作属实有点憨憨,所以一定要封装。

3.补充功能

有时候我们的组件还需要拓展功能,这种场景也是需要封装的,比如antd表格的基础表格是没有筛选的,
学会正确地二次封装组件,让同事抱着你的大腿喊大神!_第3张图片
要实现列筛选功能,是需要我们自己手写逻辑的,那么我们就应该把这段补充的逻辑封装在组件里。

二、二次封装的原则事项

1.必须暴露组件的所有props、事件、插槽和方法

如果做二次封装,没有暴露组件库本身的属性和方法。那说实话,你这不叫封装组件,你这叫“坐牢”,只有封没有装那能叫封装吗?

还是以封装antd的输入框为例,我只是对样式做了一些更改,那么antd输入框中的props、事件、插槽和方法,是应该暴露出来给父组件的,否则封装的组件压根就没法用。
学会正确地二次封装组件,让同事抱着你的大腿喊大神!_第4张图片

那我这里怎么绑定呢?不可能一个个照着文档把props全绑上去吧?


//憨憨的做法:照着文档一个个地绑定props
props:{
    placeholder: {
      type: String,
      default: ''
    },
    defaultValue: {
      type: String,
      default: ''
    },
    //把所有的props一个个填上去......
}

真当程序员是码农啊?别干这些“重复”的体力活了,具体怎么做呢?这里讲一下封装技巧:

1)绑定$attrs暴露props

实现思路:用计算属性获取this.$attrs的值,然后用v-bind绑定,下面antd的弹窗组件为例:
HTML

<a-modal v-bind="_attrs">

将一些未处理的参数或特殊处理的参数绑定到 a-modal 上

computed: {
    _attrs() {
      let attrs = { ...this.$attrs }
      return attrs
    },
}

2)绑定$listeners暴露事件

<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

学会正确地二次封装组件,让同事抱着你的大腿喊大神!_第5张图片

解决:
去掉 @change=“changeSelect”, 使用 new$listeners 继承原有属性,并覆盖原有事件

xxx
computed: {
    new$listeners() {
      return Object.assign(this.$listeners, {
       //在这里覆盖原有的change事件
        change: this.changeSelect
      })
    }
  },
  method: {
	changeSelect(val) {
         //你自己的业务逻辑
         //...
	      this.$emit('change', val) 
	}
  }

3)绑定 s l o t s 和 slots和 slotsscopedSlots暴露插槽

同样的道理,插槽也是必须要暴露给父组件的,主要靠Vue提供的 s l o t s 和 slots和 slotsscopedSlots,具体方法如下:

  <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))
    },
  },

4)暴露实例的方法

拿element-ui的 「Cascader级联选择器」 来说吧,我有一个需求,需要用到getCheckedNodes()这个方法
学会正确地二次封装组件,让同事抱着你的大腿喊大神!_第6张图片

这里是我的父组件要调用这个方法,如果手动的注册事件,一层层的传递也挺麻烦。

//二次封装组件
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:别问我是怎么总结出来的,都是实战中的血与泪╮(╯﹏╰)╭

2.二次封装的原则

1)搞清楚哪些是props,不要修改props的数据

慎重封装跟业务有关的代码,你可以根据一个原则来判断,如果业务接口更新的是props中的数据,那就不要这段业务代码,放到父组件去做更好。因为这样会破坏vue的单向数据流,带来维护上的困难。
举个我自己遇到的坑,下面这个表格组件,写了一个业务接口,是更新表格的dataSource。

methods:{
    loadData(){
        getData().then((res) => {
           this.dataSource = res.simpleDataList
        })
       }
    }
}

而这个属性是实际上是antd表格组件的props
学会正确地二次封装组件,让同事抱着你的大腿喊大神!_第7张图片
我这里犯得错误是,我没有把它定义为props,而是放在了data里:

data() {
    return {
        dataSource:[]
    }
}

这就违背我上面说的第一条原则,一定要暴露封装组件的所有props,所以这个属性不应该是data,而是props。而我这里更新了本该是props的数据,这就是组件封装上很严重的问题,我的父组件想改变dataSource变得很困难,这给我后面的维护带来了无尽的折磨…

不要修改props的数据(这样写控制台会报错)
更不要修改本该是props的数据。

2)不要在vue文件里封装多个数据入口

不要在一个vue文件内,封装多个数据的入口处理,你应该遵守“单一职责”。

这次的反面教材,还是上面说的数据表组件。

由于加载数据表有很多个接口,每个接口的参数又不一样,当时就把很多个接口分别做成多个入口方法,但这样后期维护很难受,因为这个接口是应该我父组件去请求的,每次后台更新接口参数,放在子组件我就很难维护,因为跟业务耦合的太死了。
学会正确地二次封装组件,让同事抱着你的大腿喊大神!_第8张图片
所以后面这个组件我直接重写了,定位为纯展示的组件,只加了样式的优化,还有添加筛选功能。所有的数据,都由父组件来传props控制。
所以,当你发现一个组件要耦合多个数据入口时,就不要把请求写在子组件里面,而是把这个组件改造为纯展示组件,跟业务解耦,把业务请求交给父组件。

要不然随着后期业务的变化,你的维护成本会变得越来越高。

3)载体分离原则

内容和载体要分两个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>

这样就能保证表单内容组件的复用。比如你下一次不需要在弹窗里放表单了,而是直接在页面上展示,或者换成抽屉其他载体,你就不需要单独把表单这段代码复制来复制去了。

遵守载体分离原则,解耦内容和载体,就能方便后期的拓展开发。

三、二次封装的其他实操技巧

1.实现双向绑定,简化事件处理

我们封装输入框、复选框这种组件的时候,还需要保留他们的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。

2.修改组件库的样式

当修改单个文件的样式时,以 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

你可能感兴趣的:(Vue,组件设计,前端,react.js,javascript)