如何在不影响原有组件的基础上,对组件进行二次封装
在vue2中,我们可以通过extends的方式,通过给组件打补丁的方式来二次封装组件,可以参考这个视频。那么在vue3中,使用了组合式api之后,又该怎样实现同样的功能呢?
功能分析
二次封装之后,原有组件的使用方式基本上不发生改变,只对自己想要改造的地方有影响,具体分为以下几点:
- 原有组件的传参props(包括属性和事件)
- 原有组件的插槽slots(开发模式下修改插槽内容也能热更新)
- 原有组件导出的方法和属性(通过ref绑定组件获取到的内容)
1.组件传参
/* MyButton.vue */
<script setup>
import { ElButton } from 'element-plus';
defineOptions({
inheritAttrs: false, // 解决自定义封装事件多次执行
})
const props = defineProps({
onClick: {
type: Function,
default: () => {},
},
})
function onClick() { // 对按钮点击事件进行自定义封装
props.onClick?.();
}
</script>
<!-- MyButton.vue -->
<template>
<ElButton v-bind="$attrs" @click="onClick"></ElButton>
</template>
inheritAttrs设置为false时,使用MyButton组件时绑定的属性和事件就不会默认绑定到组件的根节点上,所以配合v-bind="$attrs"来实现组件的传参。$attrs能获取到除了defineProps中定义之外的属性和事件,所以defineProps中定义的props要手动绑定到组件上,这样就解决了自定义封装事件会执行多次的问题。
注意:defineOptions在vue3.3+的版本中才支持,如果是vue3.3以下的版本,可以通过在组件文件开头添加js代码块的方式配置。
/* MyButton.vue */
<script>
export default {
inheritAttrs: false,
}
</script>
2.组件插槽
/* MyButton.vue */
<script setup>
import { ref, onUpdated, useSlots } from 'vue';
const slots = useSlots(), slotNames = ref([]), _key = ref(0);
function setSlotNames() { // 动态添加插槽
const names = [];
Object.entries(slots).map(([name, slot]) => {
for (const item of slot()) {
if (!['Symbol(Comment)', 'Symbol(v-cmt)'].includes(item.type.toString())) { // 判断插槽内容是否不为注释
names.push(name);
return;
}
}
})
if (slotNames.value.sort().toString() !== names.sort().toString()) { // 判断默认插槽和具名插槽是否增加或减少
slotNames.value = names;
_key.value = Date.now(); // 设置key值重新渲染页面,保证插槽热更新
}
}
if (import.meta.env.DEV) { // 开发模式下才需要热更新插槽
onUpdated(() => {
setSlotNames()
})
}
setSlotNames();
</script>
<!-- MyButton.vue -->
<template>
<ElButton :key="_key" v-bind="$attrs" @click="onClick">
<slot :name="name" v-bind="{...scope}"></slot>
</ElButton>
</template>
开发模式下为什么需要在onUpdated中重新执行setSlotNames呢?举个例子:当我们在使用二次封装的el-table-column组件时,写了以下代码:
<template>
<el-table :data="[{ name: '小红' }]">
<el-table-column label="名称" prop="name">
<!-- 小明 -->
</el-table-column>
</el-table>
</template>
一开始表格单元格中显示的内容是小红,当我们把注释<!-- 小明 -->放开之后,单元格中预期的内容应该变为小明。假设不执行setSlotNames重新设置插槽,那么单元格内容将不会发生改变。生产环境中不需要在onUpdated中重新执行setSlotNames,避免造成性能浪费。
3.组件导出的方法和属性
/* MyButton.vue */
<script setup>
import { ref, onMounted } from 'vue';
const _ref = ref(null);
const exposeObj = {
};
onMounted(() => {
// 某些组件直接遍历 _ref.value 控制台会有一行警告,比如 el-table-column 组件
const ctx = Object.prototype.hasOwnProperty.call(_ref.value, '$') ? _ref.value.$.ctx : _ref.value;
Reflect.ownKeys(ctx).forEach(key => {
if (!Reflect.has(exposeObj, key)) exposeObj[key] = ctx[key];
});
});
defineExpose(exposeObj);
</script>
<!-- MyButton.vue -->
<template>
<ElButton ref="_ref" v-bind="$attrs"></ElButton>
</template>
这样通过ref拿到MyButton组件的方法和属性就包含了原有组件中的方法和属性,也可以在exposeObj中扩展想要暴露出来的方法和属性。
完整代码
/* MyButton.vue */
<script setup>
import {ref, onMounted, onUpdated, useSlots, useAttrs} from 'vue';
import { ElButton } from 'element-plus';
defineOptions({
inheritAttrs: false, // 解决自定义封装事件多次执行
})
const props = defineProps({
onClick: {
type: Function,
default: () => {},
},
})
function onClick() { // 对按钮点击事件进行自定义封装
props.onClick?.();
}
const slots = useSlots(), slotNames = ref([]), _key = ref(0);
function setSlotNames() { // 动态添加插槽
const names = [];
Object.entries(slots).map(([name, slot]) => {
for (const item of slot()) {
if (!['Symbol(Comment)', 'Symbol(v-cmt)'].includes(item.type.toString())) { // 判断插槽内容是否不为注释
names.push(name);
return;
}
}
})
if (slotNames.value.sort().toString() !== names.sort().toString()) { // 判断默认插槽和具名插槽是否增加或减少
slotNames.value = names;
_key.value = Date.now(); // 设置key值重新渲染页面,保证插槽热更新
}
}
if (import.meta.env.DEV) { // 开发模式下才需要热更新插槽
onUpdated(() => {
setSlotNames()
})
}
setSlotNames();
const _ref = ref(null);
const exposeObj = {
};
onMounted(() => {
// 某些组件直接遍历 _ref.value 控制台会有一行警告,比如 el-table-column 组件
const ctx = Object.prototype.hasOwnProperty.call(_ref.value, '$') ? _ref.value.$.ctx : _ref.value;
Reflect.ownKeys(ctx).forEach(key => {
if (!Reflect.has(exposeObj, key)) exposeObj[key] = ctx[key];
});
});
defineExpose(exposeObj);
</script>
<!-- MyButton.vue -->
<template>
<ElButton ref="_ref" :key="_key" v-bind="$attrs" @click="onClick">
<slot :name="name" v-bind="{...scope}"></slot>
</ElButton>
</template>
进阶封装
使用v-bind="$attrs"绑定组件传参时,在外部使用组件不会出现原有组件的属性提示,这时可以将组件传参改成:
/* MyButton.vue */
<script setup>
import { ref, useAttrs } from 'vue';
import {ElButton, buttonProps} from 'element-plus';
const props = defineProps(Object.assign({}, buttonProps, {
onClick: {
type: Function,
default: () => {},
},
}));
function onClick() {
props.onClick?.();
}
const attrs = useAttrs(), _attrs = {};
for (const k in props) {
if (!k.startsWith('on')) _attrs[k] = props[k];
}
for (const k in attrs) {
if (!(k in props)) _attrs[k] = attrs[k];
}
</script>
<!-- MyButton.vue -->
<template>
<ElButton v-bind="_attrs" @click="onClick"></ElButton>
</template>
然后再将MyButton.vue中复用的代码抽离出来,封装成一个hook,最终完整的代码如下:
/* hooks.js */
import { ref, onMounted, onUpdated, useAttrs, useSlots } from 'vue';
export function usePackage(props, exposeObj) {
const attrs = useAttrs(), _attrs = {};
for (const k in props) {
if (!k.startsWith('on')) _attrs[k] = props[k];
}
for (const k in attrs) {
if (!(k in props)) _attrs[k] = attrs[k];
}
const slots = useSlots(), slotNames = ref([]), _key = ref(0);
function setSlotNames() {
const names = [];
Object.entries(slots).map(([name, slot]) => {
for (const item of slot()) {
if (!['Symbol(Comment)', 'Symbol(v-cmt)'].includes(item.type.toString())) {
names.push(name);
return;
}
}
})
if (slotNames.value.sort().toString() !== names.sort().toString()) {
slotNames.value = names;
_key.value = Date.now();
}
}
if (import.meta.env.DEV) {
onUpdated(() => {
setSlotNames()
})
}
setSlotNames();
const _ref = ref(null);
onMounted(() => {
const ctx = Object.prototype.hasOwnProperty.call(_ref.value, '$') ? _ref.value.$.ctx : _ref.value;
Reflect.ownKeys(ctx).forEach(key => {
if (!Reflect.has(exposeObj, key)) exposeObj[key] = ctx[key];
});
});
return {_key, _attrs, slotNames, _ref};
}
/* MyButton.vue */
<script setup>
import { ElButton, buttonProps } from 'element-plus';
import { usePackage } from './hooks.js';
const props = defineProps(Object.assign({}, buttonProps, {
onClick: {
type: Function,
default: () => {},
},
}));
function onClick() {
props.onClick?.();
}
const exposeObj = {
};
const {_key, _attrs, slotNames, _ref} = usePackage(props, exposeObj);
defineExpose(exposeObj);
</script>
<!-- MyButton.vue -->
<template>
<ElButton ref="_ref" :key="_key" v-bind="_attrs" @click="onClick">
<template v-for="name in slotNames" #[name]="scope">
<slot :name="name" v-bind="{...scope}"></slot>
</template>
</ElButton>
</template>
1709

被折叠的 条评论
为什么被折叠?



