Vue必须会的组件封装

作为一个前端开发,必须会的vue组件封装技能

一、v-modal组件封装

vue2 的写法:

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>
<my-input v-model="msg"></my-input>
</template>
<script>
export default {
data() {
return {
msg: ''
}
}
}
</script>

<!-- 子组件 -->
<template>
<el-input v-model="inputVal"></el-input>
</template>
<script>
export default {
computed: {
inputVal: {
get() {
return this.props.value
},
set(val) {
this.$emit('input', val)
}
}
}
}
</script>

vue3 的写法:

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
<!-- 父组件 -->
<template>
<my-input v-model="msg"></my-input>
</template>
<script setup lang="ts">
import { ref } from 'vue'
const msg = ref('hello')
</script>

<!-- 子组件 -->
<template>
<el-input v-model="inputVal"></el-input>
</template>
<script setup lang="ts">
import { computed } from 'vue';
const props = defineProps({
modelValue: {
type: String,
default: '',
}
});

const emit = defineEmits(['update:modelValue']);

const inputVal = computed(() => {
get() {
return props.modelValue
},
set(val) {
emit('update:modelValue', val)
}
})
</script>

二、如何二次扩展组件

在开发 Vue 项目中我们一般使用第三方 UI 组件库进行开发,如 Element-Plus, 但是这些组件库提供的组件并不一定满足我们的需求,这时我们可以通过对组件库的组件进行二次封装,来满足我们特殊的需求。
对于封装组件有一个大原则就是我们应该尽量保持原有组件的接口,除了我们需要封装的功能外,我们不应该改变原有组件的接口,即保持原有组件提供的接口(属性、方法、事件、插槽)不变。

属性透传 $attrs

$attrs 的官方解释:包含了父作用域中不作为组件 props 或自定义事件的 attribute 绑定和事件;当一个组件没有声明任何 prop 时,这里会包含所有父作用域的绑定,并且可以通过 v-bind="$attrs" 传入内部的 UI 组件中

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
<template>
<div class="my-input">
<el-input v-bind="filteredAttrs"></el-input>

<!-- 如果不希望过滤掉某些属性 可以直接使用 $attrs -->
<el-input v-bind="$attrs"></el-input>
</div>
</template>

<script lang="ts" setup>
import {useAttrs,computed,ref } from 'vue'
import { ElInput } from 'element-plus'
defineOptions({
name: 'MyInput'
})

// 接收 name,其余属性都会被透传给 el-input
defineProps({
name: String
});

// 如果我们不希望透传某些属性比如class, 我们可以通过useAttrs来实现
const attrs = useAttrs()
const filteredAttrs = computed(() => {
return { ...attrs, class: undefined };
});
</script>

属性传递时,在默认情况下,父组件的属性会直接渲染在子组件的根节点上,但是有些情况我们希望是渲染在指定的节点上,可以使用 $attrsinheritAttrs: false 就可以完美的解决这个问题。

Event事件继承 $listeners

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// Vue2
<template>
<div class="my-input">
<el-input v-bind="$attrs" v-on="$listeners"></el-input>
</div>
</template>

// Vue3
<template>
<div class="my-input">
<!-- 在 Vue3 中,取消了$listeners这个组件实例的属性,将其事件的监听都整合到了$attrs上 -->

<!-- 因此直接通过v-bind=$attrs属性就可以进行props属性和event事件的透传 -->
<el-input v-bind="$attrs"></el-input>

</div>
</template>

插槽传递 $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
<template>
<div class="my-input">
<el-input
v-model="childSelectedValue"
v-bind="attrs"
v-on="$listeners"
>
<!-- 遍历子组件非作用域插槽,并对父组件暴露 -->
<template v-for="(index, name) in $slots" v-slot:[name]>
<slot :name="name" />
</template>
<!-- 遍历子组件作用域插槽,并对父组件暴露 -->
<!-- 作用域插槽是什么?父组件通过在子组件中使用插槽填充内容的时候,可以通过slot-scope属性获取 子组件中<slot>标签 通过属性 传递过来的子组件的作用域 的值。 -->
<template v-for="(index, name) in $scopedSlots" v-slot:[name]="data">
<slot :name="name" v-bind="data"></slot>
</template>
</el-input>
</div>
</template>

<!-- 在 Vue3 中,取消了作用域插槽 $scopedSlots,将所有插槽都统一在 $slots 当中: -->
<template>
<div class="my-input">
<el-input
v-model="childSelectedValue"
v-bind="attrs"
v-on="$listeners"
>
<template #[slotName]="slotProps" v-for="(slot, slotName) in $slots" >
<slot :name="slotName" v-bind="slotProps"></slot>
</template>
</el-input>
</div>
</template>

使用第三方组件的Methods

有些时候我们想要使用组件的一些方法,比如 el-table 提供9个方法,如何在父组件(也就是封装的组件)中使用这些方法呢?其实可以通过 ref 链式调用,比如 this.$refs.tableRef.$refs.table.clearSort(),但是这样太麻烦了,代码的可读性差;更好的解决方法:将所有的方法暴露出来,供父组件通过 ref 调用!

在 Vue2 中,可以将 el-table 提供方法提取到实例上:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<template>
<div class="my-table">
<el-table ref="el-table"></el-table>
</div>
</template>

<script>
export default {
mounted() {
this.extendMethod()
},
methods: {
extendMethod() {
const refMethod = Object.entries(this.$refs['el-table'])
for (const [key, value] of refMethod) {
if (!(key.includes('$') || key.includes('_'))) {
this[key] = value
}
}
},
};
</script>

在 Vue3 中的使用方法如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<template>
<div class="my-table">
<el-table ref="table"></el-table>
</div>
</template>

<script lang="ts" setup>
import { ref, onMounted } from 'vue'
import { ElTable } from 'element-plus'

const table = ref();

onMounted(() => {
const entries = Object.entries(table.value);
for (const [method, fn] of entries) {
expose[method] = fn;
}
});
defineExpose(expose);
</script>