vue 中完美地二次封装 UI 组件

当我们全局引用UI框架(类似于 Quasar Framework)的时候,为了使得整个项目风格统一,需要对某些组件进行二次封装,使得可以集中管理组件风格,使得代码易于维护。

封装需求

  • 属性传递

    二次封装后的组件与被封装组件和具有同样的参数

    1
    2
    <xxx-xxx v-bind="$attrs">
    </xxx-xxx>
  • 事件传递

    1
    2
    <xxx-xxx v-on="$listeners">
    </xxx-xxx>
  • 插槽传递

    1
    2
    3
    4
    5
    <xxx-xxx>
    <template v-for="name in $scopedSlots" :slot="name">
    <slot :name="name" />
    </template>
    </xxx-xxx>

vue 相关知识点

vm.$attrs

官方解释:

包含了父作用域中不作为 prop 被识别 (且获取) 的 attribute 绑定 (classstyle 除外)。当一个组件没有声明任何 prop 时,这里会包含所有父作用域的绑定 (classstyle 除外),并且可以通过 v-bind="$attrs" 传入内部组件——在创建高级别的组件时非常有用。

通俗的理解就是,子组件可以通过 $attrs 可以访问父组件传过来的所有属性,但需要注意的是如果父组件所传的属性中有在子组件 props 中有过声明,那么该属性不会出现在 $attrs 对象中。

vm.$listeners

官方解释:

包含了父作用域中的 (不含 .native 修饰器的) v-on 事件监听器。它可以通过 v-on="$listeners" 传入内部组件——在创建更高层次的组件时非常有用。

vm.$props

官方解释:

当前组件接收到的 props 对象。Vue 实例代理了对其 props 对象 property 的访问。

inheritAttrs

官方的解释让人看着头大,通俗来讲,其实默认情况就是把 $attrs 对象上没在子组件 props 中声明的属性加在子组件的根 html 标签上。

vm.$scopedSlots

官方解释:

用来访问作用域插槽。对于包括 默认 slot 在内的每一个插槽,该对象都包含一个返回相应 VNode 的函数。

通俗讲,就是通过该属性,可以访问所有的插槽。

封装示例

为了能够实现上述两个需求,我们使用 $attrs$props$listeners 这三个属性来实现。下文将通过封装 quasar 组件中的 QTable 来举例说明

原始组件

QTable API 中我们可以知道,其有63个属性,19 个插槽,9 个事件,在封装的时候,我们需要通过预设一些属性,使得表格符合我们的使用,但是又得保证其灵活性。

image-20220406214311656

二次封装

具体解释见里面的备注

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
<template>
<q-table v-bind="$attrs" :dense="dense" v-on="$listeners">
<!--示例: 在封装组件中增加插槽,通过后备内容进行自定义,方便父组件覆盖当前插槽-->
<template v-slot:top="props">
<slot name="top" v-bind="props">
<q-space />
<q-input
v-model="filter"
dense
debounce="300"
placeholder="搜索"
color="primary"
>
<template v-slot:append>
<q-icon name="search" />
</template>
</q-input>
</slot>
</template>

<!--根据父类插槽定义,传递插槽到被封装组件-->
<template v-for="slotName in scopedSlotsName" v-slot:[slotName]="props">
<!-- v-bind 是向插槽中传递参数,使得父类的插槽可以使用-->
<slot :name="slotName" v-bind="props" />
</template>
</q-table>
</template>

<script>
export default {
// 默认为 true,为 true 时会把 `$attrs` 对象上没在子组件 `props` 中声明的属性加在子组件的根 `html` 标签上。
inheritAttrs: false,

props: {
// 设置组件的默认值
dense: {
type: Boolean,
default() {
return true
}
}
},

data() {
return {
filter: 'filter2'
}
},

computed: {
attrs() {
// 因为 $attrs 不包含 $props 中的值,在此处对属性进行合并,然后供被封装组件使用
// 由于 Object.assign 是浅复制,所以不会影响字段的 getter 和 setter
return Object.assign({}, this.$attrs, this.$props)
},

// 作用域内的插槽名称
scopedSlotsName() {
let keys = Object.keys(this.$scopedSlots)
// 过滤掉以$开头的字段,$ 开头的是 vue 框架的值
keys = keys.filter(key => !key.startsWith('$'))
// 过滤掉已经添加插槽名称
const existSlotNames = ['top']
keys = keys.filter(key => !existSlotNames.includes(key))

return keys
}
}
}
</script>

<style scoped>
</style>>

组件使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
<template>
<wrapped-table :data="data" :columns="columns" :dense="dense">
<template v-slot:body-cell-index="props">
<q-td>{{ props.value }}_1</q-td>
</template>
</wrapped-table>
</template>

<script>
import WrappedTable from './index.vue'

export default {
components: { WrappedTable },

data() {
return {
columns: [
{
name: 'index',
label: '序号',
align: 'left',
field: 'name'
},
{
name: 'name',
align: 'center',
label: '名称',
field: 'name',
sortable: true
}
],
data: [
{
index: 1,
name: 'Frozen Yogurt'
},
{
index: 1,
name: 'Ice cream sandwich'
}
],

dense: false
}
}
}
</script>

<style scoped>
</style>

参考

  1. 解决Vue2.x中二次封装Vue组件时批量继承属性,方法,插槽的方法
  2. Vue二次封装组件,并传递props和v-on事件
  3. 基于UI库二次组件封装 - SegmentFault 思否
  4. 浅谈 Vue2.4.0 $attrs 与 inheritAttrs - SegmentFault 思否
  5. 插槽 — Vue.js (vuejs.org)