司素浏览器
61.24M · 2026-02-07
在Vue/React项目开发中,组件库(Element Plus、Ant Design等)二次封装是常态——为了统一项目样式、复用业务逻辑、简化使用成本,我们常会基于基础组件封装业务组件。
但封装过程中,几乎所有开发者都会遇到同样的困境:attrs透传丢失、事件冲突不触发、slots插槽错乱、TS类型报错……这些痛点看似细小,却会导致封装组件易用性骤降、维护成本翻倍,甚至违背二次封装的初衷。
本文聚焦组件库二次封装最核心的4大痛点,结合Vue3实操案例,从“痛点分析+解决方案”双维度拆解,新手也能直接套用,彻底告别封装内耗。
以下4大痛点,覆盖组件库二次封装80%的高频问题,优先解决“实用性”,所有方案均适配Vue3(<script setup>+TS),兼顾易用性和规范性。
基于基础组件封装时,父组件传递的额外属性(如placeholder、disabled、class),无法透传到底层基础组件,导致基础组件功能失效。
示例:封装ElInput组件,父组件传递placeholder,却无法显示。
<!-- 封装组件 MyInput.vue(有问题写法) -->
<template>
<ElInput v-model="inputVal" /> <!-- 未透传attrs,父组件传递的placeholder无法生效 -->
</template>
<script setup>
const props = defineProps(['modelValue'])
const emit = defineEmits(['update:modelValue'])
const inputVal = toRef(props, 'modelValue')
</script>
<!-- 父组件使用 -->
<MyInput v-model="val" placeholder="请输入内容" /> <!-- placeholder不显示 -->
</code>解决方案:v-bind="$attrs" 完整透传Vue3中,$attrs包含父组件传递的所有未被props声明的属性,通过v-bind="$attrs",可将所有attrs一次性透传到底层基础组件,同时注意inheritAttrs的合理使用。<!-- 封装组件 MyInput.vue(正确写法) -->
<template>
<!-- 核心:v-bind="$attrs" 透传所有未声明的属性 -->
<ElInput v-model="inputVal" v-bind="$attrs" />
</template>
<script setup>
import { toRef } from 'vue'
// 仅声明需要处理的props,其余属性自动进入$attrs
const props = defineProps(['modelValue'])
const emit = defineEmits(['update:modelValue'])
const inputVal = toRef(props, 'modelValue')
// 可选:若需自定义attrs透传(如剔除部分属性)
const attrs = useAttrs()
// 解构剔除不需要透传的属性,再透传剩余部分
const { class: _, ...restAttrs } = attrs
// <ElInput v-model="inputVal" v-bind="restAttrs" />
</script>
关键注意:inheritAttrs默认值为true,若设置为false,需手动透传class/style(Vue3中$attrs已包含class/style),避免样式丢失。
封装组件时,底层基础组件的事件(如ElButton的click)与封装组件自身的事件重名,导致父组件绑定的事件不触发,或触发异常;甚至出现“多次触发”的问题。
示例:封装ElButton,自身绑定click事件处理业务逻辑,父组件绑定的click事件无法触发。
核心思路:封装组件自身的事件处理完成后,通过$emit透传底层组件的事件;若需合并事件,可使用展开运算符,将底层组件的事件一次性透传。
<!-- 封装组件 MyButton.vue(正确写法) -->
<template>
<!-- 核心:@click="handleClick" 处理自身业务,同时透传底层事件 -->
<ElButton
v-bind="$attrs"
@click="handleClick"
@blur="$emit('blur')" <!-- 透传单个事件 -->
v-on="$listeners" <!-- Vue2写法,Vue3可省略,$attrs已包含事件 -->
>
<slot />
</ElButton>
</template>
<script setup>
const emit = defineEmits(['click', 'blur'])
// 自身业务逻辑处理
const handleClick = (e) => {
// 1. 处理封装组件的业务逻辑(如权限判断、加载状态)
console.log('处理业务逻辑')
// 2. 透传click事件给父组件,确保父组件绑定的事件触发
emit('click', e)
}
// 若需合并多个事件(简化写法)
const emits = defineEmits(['click', 'blur', 'focus'])
// 底层组件的所有事件,一次性透传
// <ElButton v-bind="$attrs" v-on="$listeners" />
</script>
关键注意:Vue3中,listeners已被合并到attrs" 同时透传属性和事件,无需额外写v-on="$listeners"。
基础组件的具名插槽(如ElSelect的prefix、suffix插槽),在封装后无法被父组件正常使用;或封装组件自身的插槽与底层组件插槽冲突,导致内容渲染错位。
示例:封装ElSelect,父组件无法使用prefix插槽添加前缀图标。
核心思路:封装组件中,保留底层组件的所有插槽,通过标签透传,默认插槽直接用,具名插槽需指定name属性,确保父组件可正常使用。
<!-- 封装组件 MySelect.vue(正确写法) -->
<template>
<ElSelect v-model="value" v-bind="$attrs" @change="$emit('change')">
<!-- 1. 透传默认插槽(选项列表) -->
<slot/>
<!-- 2. 透传具名插槽(prefix、suffix等,底层组件有的都要透传) -->
<template #prefix>
<slot name="suffix"/> <!-- 父组件可通过#prefix使用 -->
</template>
<template #suffix>
<slot name="suffix"/>
</template>
<!-- 3. 封装组件自身的插槽(可自定义名称,避免冲突) -->
<template #myCustom>
<span>封装组件自身的插槽内容</span>
</template>
</ElSelect>
</template>
<script setup>
const props = defineProps(['modelValue'])
const emit = defineEmits(['update:modelValue', 'change'])
const value = toRef(props, 'modelValue')
</script>
关键注意:具名插槽必须“一一对应”,底层组件有多少个具名插槽,封装组件就需要透传多少个;若无需自定义处理,可直接用 透传,无需额外嵌套。
使用TS开发时,封装组件无法继承底层基础组件的类型,导致父组件传递props、事件时,没有类型提示、类型报错;甚至出现“传错参数”却无法提前发现的问题,违背TS的类型安全理念。
示例:封装ElInput,父组件传递type="textarea"时,TS提示“类型不存在”。
核心思路:通过Vue3提供的ComponentProps、ComponentEmits等工具类型,继承底层基础组件的props、emits类型,再扩展封装组件自身的类型,实现类型全覆盖。
<!-- 封装组件 MyInput.vue(TS正确写法) -->
<template>
<ElInput v-model="inputVal" v-bind="$attrs" @input="$emit('input')" />
</template>
<script setup>
import { toRef } from 'vue'
import { ElInput } from 'element-plus'
// 1. 继承ElInput的props类型,再扩展自身需要的props
type MyInputProps = ComponentProps<typeof ElInput> & {
// 封装组件自身新增的props,可选
customProp?: string
}
// 2. 继承ElInput的emits类型,再扩展自身的emits
type MyInputEmits = ComponentEmits<typeof ElInput> & {
// 封装组件自身新增的事件,可选
customEmit?: (value: string) => void
}
// 3. 应用类型
const props = defineProps<MyInputProps>()
const emit = defineEmits<MyInputEmits>()
const inputVal = toRef(props, 'modelValue')
</script>
关键注意:
defineProps<ComponentProps<typeof ElInput>>(),无需额外扩展。解决痛点的同时,遵循以下规范,可让封装的组件更易用、更易维护,避免后续踩新坑:
组件库二次封装的核心,是“复用+简化”,而不是“复杂化”。记住以下4个核心要点,就能避开80%的坑:
其实组件库二次封装并不复杂,只要吃透这4大痛点的解决方案,再遵循通用规范,就能封装出易用、易维护的业务组件,既提升开发效率,又保证项目规范性。新手建议先从简单组件(如按钮、输入框)入手,熟练后再封装复杂组件(如表格、表单)~