记录:如何包裹一个现有的Vue组件

背景

有点高阶组件的感觉,把一个组件功能增强或修改组件的一些默认配置,同时外界能像原有组件那样使用新的组件。不过高阶组件是一个函数,而我们是直接写一个新的 Vue 组件,相当于是直接写这个函数的返回值了。

假设有一个 Input 组件,可以使用 v-model ,可以传 Props ,可以触发事件,可以传 Slots (包括 scopedSlots)。
但是我们在使用过程中发现,它的配置太自由了,但默认值不是我们想用的,每次使用都要配置一次,而且我们希望旁边能有个按钮,加个搜索功能。

于是我们新建一个 Vue 组件 SearchInput

Template

首先来实现新组件的 DOM 结构:

1
2
3
4
5
6
<template>
<div>
<Input/>
<button @click="handleSearch">搜索</button>
</div>
</template>

Props

然后接收 Input 组件所有的 Props ,甚至你还想加几个 Props 上去:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<template>
<div>
<Input
v-bind="$attrs"
/>
<button
v-if="canSearch"
@click="handleSearch"
>搜索</button>
</div>
</template>

<script>
export default {
name: 'SearchInput',
props: {
canSearch: {
type: Boolean,
default: true,
},
},
}
</script>

Events

使用 $listeners 把监听器都挂 Input 上:

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
<template>
<div>
<Input
v-bind="$attrs"
v-on="$listeners"
/>
<button
v-if="canSearch"
@click="handleSearch"
>搜索</button>
</div>
</template>

<script>
export default {
name: 'SearchInput',
props: {
canSearch: {
type: Boolean,
default: true,
},
},
methods: {
handleSearch () {
this.$emit('search')
},
},
}
</script>

也可以在 v-on 上绑定一个 computed ,对 $listeners 做手脚。

v-model

Vue 版本 2.6.2 以后直接

1
2
v-bind="$attrs"
v-on="$listeners"

一般就可以实现 v-model 了,涉及原生的可能需要重写 input 事件监听器。

2.6.2 以前的版本有个 issue #8430 ,在外部使用 v-model 不会把 value prop 传入 $attrs 中,因此需要手动声明 value prop 兼容:

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
<template>
<div>
<Input
:value="value"
v-bind="$attrs"
v-on="$listeners"
/>
<button
v-if="canSearch"
@click="handleSearch"
>搜索</button>
</div>
</template>

<script>
export default {
name: 'SearchInput',
props: {
value: {},

canSearch: {
type: Boolean,
default: true,
},
},
methods: {
handleSearch () {
this.$emit('search')
},
},
}
</script>

Slots

在 Vue 2.6 以后推出了新语法 v-slot ,因此我们可以这样传入所有 slots :

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
<template>
<div>
<Input
:value="value"
v-bind="$attrs"
v-on="$listeners"
>
<template
v-for="(_, slotName) in $scopedSlots"
v-slot:[slotName]="slotProps"
>
<slot
:name="slotName"
v-bind="slotProps"
></slot>
</template>
</Input>
<button
v-if="canSearch"
@click="handleSearch"
>搜索</button>
</div>
</template>

<script>
export default {
name: 'SearchInput',
props: {
value: {},

canSearch: {
type: Boolean,
default: true,
},
},
methods: {
handleSearch () {
this.$emit('search')
},
},
}
</script>

(在此吐槽一句,新语法就意味着新坑)

2.6 以下的,老样子,把 $slots, $scopedSlots 分别传入:

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
<template>
<div>
<Input
:value="value"
v-bind="$attrs"
v-on="$listeners"
>
<slot
v-for="(_, slotName) in $slots"
:name="slotName"
:slot="slotName"
></slot>
<template
v-for="(_, slotName) in $scopedSlots"
:slot="slotName"
slot-scope="slotProps"
>
<slot
:name="slotName"
v-bind="slotProps"
></slot>
</template>
</Input>
<button
v-if="canSearch"
@click="handleSearch"
>搜索</button>
</div>
</template>

<script>
export default {
name: 'SearchInput',
props: {
value: {},

canSearch: {
type: Boolean,
default: true,
},
},
methods: {
handleSearch () {
this.$emit('search')
},
},
}
</script>

至此就完成了包裹。