携手创作,一同成长!这是我参与「日新计划 8 月更文挑战」的第1天,点击检查活动详情>>

前语

常常的,咱们在日常工作中,会运用第三方UI组件库,比方:element-ui、vant-ui、iview、ant-design等等。不论是为了事务考虑仍是单纯的为了进步功率,咱们会把一些常常用到的组件抽离、封装成公共组件,这样便利咱们在不同的地方运用这个组件,减少重复代码的编写。

咱们把对于第三方组件库的封装称为组件的二次封装,那么这带来有个考虑,当咱们在二次封装时,咱们在封装什么?

二次封装时,咱们需求遵循什么?

在 vue 组件封装时,咱们需求留意的主要是三部分:prop、event、slot。

  • prop:表示组件接纳的参数,最好用目标的写法,这样能够针对每个特点设置类型、默许值或自界说校验特点的值,此外还能够经过type、validator等方法对输入进行验证;
  • event:子组件向父组件传递消息的重要途径;
  • slot:能够给组件动态刺进一些内容或组件,是完结高阶组件的重要途径;当需求多个插槽时,能够运用具名slot。

你必须要知道的 $attrs$listeners

咱们多级组件嵌套需求传递数据时,一般运用的方法是经过vuex。假如仅仅是传递数据,而不做中间处理,运用 vuex 处理,这就有点大材小用了。所以就有了 $attrs / $listeners ,一般合作 inheritAttrs 一同运用。

感觉仍是挺不流畅难明的,简略的说便是 inheritAttrs:true 承继除props之外的一切特点;inheritAttrs:false 只承继class特点。

  • $attrs: 包含了父效果域中不被以为 (且不预期为) props 的特性绑定 (class 和 style 在外),而且能够经过 v-bind="$attrs" 传入内部组件。当一个组件没有声明任何 props 时,它包含一切父效果域的绑定 (class 和 style 在外)。

  • $listeners: 包含了父效果域中的 (不含 .native 修饰符) v-on 事情监听器。它能够经过 v-on="$listeners" 传入内部组件。它是一个目标,里边包含了效果在这个组件上的一切事情监听器,相当于子组件承继了父组件的事情。

attrs和attrs 和 listeners 在做组件二次封装时非常有用。

怎么运用 $attrs$listeners

上面说了那么多,咱们来看一个比如:

在运用 el-input-number时,当咱们给他赋默许值 null 或者空字符串 "" 时,会显现 0 ,而这在咱们一些事务场景里并不是很友爱,而且值是居中显现的,那么现在咱们想要做的改造是:值居左显现,没有默许值显现0的问题,且默许不展现操控按钮

  • 操控按钮默许不显现:controls 设置成 false
  • 居左显现:经过样式操控
  • 默许值显现0的问题:经过 computed 计算不为 number 类型时,赋值为 undefined 解决

v-model 是一个语法糖,能够拆解为 props: value 和 events: input。便是说组件只需提供一个名为 value 的 prop,以及名为 input 的自界说事情

下面开始咱们对 el-input-number 的封装:

<template>
  <el-input-number class="cz-input-number"  :controls='controls' :value='num' @input="$emit('input',$event)"></el-input-number>
</template>
<script>
export default {
  props: {
    name: 'CzInputNumber',
    value: [String, Number],
    controls: {
      type: Boolean,
      default: false
    }
  },
  computed: {
    num() {
      return typeof this.value === 'number' ? this.value : undefined
    }
  }
}
</script>
<style lang="scss" scoped>
.cz-input-number {
  ::v-deep .el-input__inner {
    text-align: left;
  }
}
</style>

上面 @input=”emit(′input′,emit(‘input’,event)” 的参数 $event 其实便是咱们在输入框输入的值,这是由于el-input-number 内部的 input 元素触发的是 “input” 自界说事情,而非原生 input 事情;此时现已是把值传递出来了,也便是 event.target.value,一切在自界说组件的回调函数中能够直接接纳这个 value 值。有兴趣的小伙伴能够去 element 对应的源码里看一下。(准确的来说是el-input 的源码,由于el-input-number 内部也是根据 el-input 的二次封装)

经过对 el-input-number 二次封装,咱们的需求其完成已基本完结了,可是咱们期望其用法坚持和 el-input-number 组件类似,这样即便其他人在运用咱们封装的组件时,也能参照 element 对应的文档正常将其运用起来。

二次封装尽量遵循的应该是原有根底的扩展,不论是为了针对事务仍是为了便利运用,而不是为组件重新制定一套新的用法,究竟封装的本质是为了进步运用体会,而不是添加更多不必要的心智担负。

这儿咱们就要用到上面介绍的 $attrs$listeners

  • $attrs “承继“ el-input-number 原有组件一切的 v-bind 特点
  • $listeners “承继” el-input-number 原有组件一切 v-on 的事情

咱们为组件添加 v-bind="$attrs"v-on="$listeners"

<template>
  <el-input-number class="cz-input-number" v-bind="$attrs" v-on="$listeners" :controls='controls' :value='num' @input="$emit('input',$event)"></el-input-number>
</template>
<script>
export default {
  name: 'CzInputNumber',
  props: {
    value: [String, Number],
    controls: {
      type: Boolean,
      default: false
    }
  },
  computed: {
    num() {
      return typeof this.value === 'number' ? this.value : undefined
    }
  }
}
</script>
<style lang="scss" scoped>
.cz-input-number {
  ::v-deep .el-input__inner {
    text-align: left;
  }
}
</style>

为了便利运用,对于常常运用的组件,我现已把他封装为了全局组件,所以直接经过 name 运用

<template>
<cz-input-number placeholder='请输入数量'  @change="change" v-model="num"></cz-input-number>
</template>
<script>
export default {
  data() {
    return {
      num: null
    }
  },
  methods: {
    change(val) {
      console.log(val, typeof val)
    }
  }
}
</script>

效果:

当咱们对组件二次封装时咱们在封装什么

能够看到,咱们传入初始值 null 时现已不会有默许显现 0 的情况了,没有在 props 里界说的 placeholder 经过 $attrs 的透传也收效了,再试验一下,change事情也能够正常触发。ok 高雅,咱们的对 el-input-number 的二次封装高雅完结。

一个考虑

已然咱们知道v-model是v-bind以及v-on合作运用的语法糖。那是不是咱们也能够运用 $attrs$listeners 替咱们完结 v-model 呢,答案当然是能够的

假如上面咱们封装的 cz-number-input 不需求对初始值做处理,那么完全能够去掉 props中 value 的界说及 @input=”emit(′input′,emit(‘input’,event)” 的事情。

穿透一层组件完结 v-model

再次举个简略的比如,咱们期望 el-input 默许可清空,即clearable默以为ture,咱们根据其做二次封装如下:

<template>
  <el-input v-bind='$attrs' v-on="$listeners" :clearable="clearable">
  </el-input>
</template>
<script>
  export default {
    name:'CzInput',
    props:{
      clearable:{type:Boolean,default:true}
    }
  }
</script>

运用:

<template>
  <CzInput placeholder="请输入内容" v-model="value"></CzInput>
</template>
<script>
  export default {
    data() {
      return {
        value: '默许可清空'
      }
    }
  }
</script>

效果:

当咱们对组件二次封装时咱们在封装什么

能够看到,咱们运用 CzInput 时,v-model 是完全没问题的,这儿咱们父组件是 CzInput ,穿透 el-input 这一层,抵达孙子原生 input 完结了v-model,并不需求重新界说 value 特点及 input 事情。

原因:父组件更改了数据,会由于 $atrrs 传递到子孙组件。而子孙组件 emit 一个 input 事情,会由于 $listeners 冒到父组件处。又由于父组件的 v-model 而主动把新数据赋值到父组件变量上,因此完结了所谓的”双向绑定”。

slot 插槽

上面咱们对 el-input 进行了简略的二次封装,所封装组件现已承继了 el-input 的一切特点及事情,可是 el-input 为了更便运用户自界说还提供了一系列插槽,所以咱们的封装也应该承继这些插槽。

一般插槽

<!-- 在组件中创立新的对应称号的插槽 -->
<template #slotName>
<!-- 在插槽内部运用对应称号的插槽 -->
    <slot name="slotName" />
</template>

slotName 为咱们的插槽称号,默许插槽时称号为 default 或可不写

动态插槽

假如需求传递的slot不固定或者较多,咱们能够经过动态插槽称号透传

<template #[slotName] v-for="(slot, slotName) in $slots" >
    <slot :name="slotName" />
</template>

这儿咱们把前面封装的 CzInput 加上插槽

<template>
  <el-input v-bind='$attrs' v-on="$listeners" :clearable="clearable">
    <template #[slotName] v-for="(slot, slotName) in $slots" >
      <slot :name="slotName" />
    </template>
  </el-input>
</template>
<script>
  export default {
    name:'CzInput',
    props:{
      clearable:{type:Boolean,default:true}
    }
  }
</script>

咱们运用一下,为组件加一个后置的按钮:

<template>
  <CzInput placeholder="请输入内容" v-model="value">
    <el-button slot="append" icon="el-icon-search"></el-button>
  </CzInput>
</template>
<script>
  export default {
    data() {
      return {
        value: '默许可清空'
      }
    }
  }
</script>

效果图:

当咱们对组件二次封装时咱们在封装什么

能够看到,插槽运用也是没有问题的

效果域插槽

假如需求封装组件运用了效果域插槽,咱们能够经过以下方法完结

<template #[slotName]="slotProps" v-for="(slot, slotName) in $slots" >
    <slot :name="slotName" v-bind="slotProps"/>
</template>

具体运用示例能够看之前这篇文章里讲到的 vue 项目开发,我遇到了这些问题

小结

  • 运用 $attrs 承继父组件的特点
  • 运用 $listeners 承继父组件的事情
  • 二次封装时插槽的传递

二次封装尽量遵循的应该是在原组件根底的扩展,不论是为了针对事务仍是为了便利运用,而不是为组件重新制定一套新的用法,究竟封装的本质是为了进步运用体会,而不是添加更多不必要的心智担负。

往期回顾

2022年了,我才开始学 typescript ,晚吗?(7.5k字总结)
vue 项目开发,我遇到了这些问题
关于首屏优化,我做了哪些