经常的,我们在日常工作中,会使用第三方UI组件库,比如:element-ui、vant-ui、iview、ant-design等等。不管是为了业务考虑还是单纯的为了提高效率,我们会把一些经常用到的组件抽离、封装成公共组件,这样方便我们在不同的地方使用这个组件,减少重复代码的编写。
我们把对于第三方组件库的封装称为组件的二次封装,那么这带来有个思考,当我们在二次封装时,我们在封装什么?
在 vue 组件封装时,我们需要注意的主要是三部分:prop、event、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 和 attrs和listeners 在做组件二次封装时非常有用。$attrs
和 $listeners
上面说了那么多,我们来看一个例子:
在使用 el-input-number时,当我们给他赋默认值 null
或者空字符串 ""
时,会显示 0 ,而这在我们一些业务场景里并不是很友好,并且值是居中显示的,那么现在我们想要做的改造是:值居左显示,没有默认值显示0的问题,且默认不展示控制按钮
v-model 是一个语法糖,可以拆解为 props: value 和 events: input。就是说组件只要提供一个名为 value 的 prop,以及名为 input 的自定义事件
下面开始我们对 el-input-number 的封装:
上面 @input=“emit(′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
了
我们为组件添加 v-bind="$attrs"
和 v-on="$listeners"
:
为了方便使用,对于经常使用的组件,我已经把他封装为了全局组件,所以直接通过 name 使用
效果:
可以看到,我们传入初始值 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’,emit(′input′,event)” 的事件。
再次举个简单的例子,我们希望 el-input 默认可清空,即clearable默认为ture,我们基于其做二次封装如下:
使用:
效果:
可以看到,我们使用 CzInput 时,v-model 是完全没问题的,这里我们父组件是 CzInput ,穿透 el-input 这一层,到达孙子原生 input 实现了v-model,并不需要重新定义 value 属性及 input 事件。
原因:父组件更改了数据,会因为
$atrrs
传递到后代组件。而后代组件 emit 一个 input 事件,会因为 $listeners 冒到父组件处。又因为父组件的 v-model 而自动把新数据赋值到父组件变量上,因此实现了所谓的"双向绑定"。
上面我们对 el-input 进行了简单的二次封装,所封装组件已经继承了 el-input 的所有属性及事件,但是 el-input 为了更方便用户自定义还提供了一系列插槽,所以我们的封装也应该继承这些插槽。
slotName 为我们的插槽名称,默认插槽时名称为 default 或可不写
如果需要传递的slot不固定或者较多,我们可以通过动态插槽名称透传
这里我们把前面封装的 CzInput 加上插槽
我们使用一下,为组件加一个后置的按钮:
效果图:
可以看到,插槽使用也是没有问题的
如果需要封装组件使用了作用域插槽,我们可以通过以下方式实现
具体使用示例可以看之前这篇文章里讲到的 vue 项目开发,我遇到了这些问题
二次封装尽量遵循的应该是在原组件基础的扩展,不管是为了针对业务还是为了方便使用,而不是为组件重新制定一套新的用法,毕竟封装的本质是为了提高使用体验,而不是增加更多不必要的心智负担。
整理了一套《前端大厂面试宝典》,包含了HTML、CSS、JavaScript、HTTP、TCP协议、浏览器、VUE、React、数据结构和算法,一共201道面试题,并对每个问题作出了回答和解析。
有需要的小伙伴,可以点击文末卡片领取这份文档,无偿分享
部分文档展示:
文章篇幅有限,后面的内容就不一一展示了
有需要的小伙伴,可以点下方卡片免费领取