【vue3二次封装组件的解决思路,以element-plus为例】

如何在不影响原有组件的基础上,对组件进行二次封装

在vue2中,我们可以通过extends的方式,通过给组件打补丁的方式来二次封装组件,可以参考这个视频。那么在vue3中,使用了组合式api之后,又该怎样实现同样的功能呢?

功能分析

二次封装之后,原有组件的使用方式基本上不发生改变,只对自己想要改造的地方有影响,具体分为以下几点:

  1. 原有组件的传参props(包括属性和事件)
  2. 原有组件的插槽slots(开发模式下修改插槽内容也能热更新)
  3. 原有组件导出的方法和属性(通过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>
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值