二次封装不是 copy 原组件的 props 和事件,而是构建「更契合业务」的组件语法糖。
在 Vue3 的日常开发中,我们经常需要对组件库(如 Element Plus、Ant Design Vue 等)做二次封装,目的是统一使用方式 、提升开发效率 ,甚至扩展一些业务逻辑。
但是封装一个组件远不只是包一层那么简单,常见的几个挑战你一定遇到过:
- 如何优雅地透传 props 和 slots?
- 如何让封装组件支持完整的类型提示?
- 怎么暴露原组件的方法供外部使用?
别急,这篇文章将带你一步步拆解 Vue3 组件封装的"硬核技能"。
🧩 场景一:优雅透传 props + attrs
在 Vue3 中,$attrs
是一个非常强大的机制,它可以让你轻松地将父组件传入的属性"传下去"给子组件,特别适合做中间层封装时使用。
常见的需求场景有:
- 你只想定义一部分常用 props,其他的由父组件直接传入;
- 你希望支持原组件的所有事件(例如
onFocus
,onInput
等),但又不想每个都手动绑定;
这时,mergeProps($attrs, props)
是最优解。它不仅能合并这两个对象,还能自动合并事件、class、style 等属性,避免冲突。
我们希望外部传入的属性能自动绑定到封装组件内部的基础组件上,同时不破坏类型提示。
👎 传统做法:
vue
<template>
<el-input :model-value="modelValue" @input="onInput" />
</template>
👎 结果:每加一个 props 或事件都要手动添加,非常繁琐且容易遗漏。
👍 推荐方案:
ts
const props = defineProps<InputProps>() // 自定义 props
vue
<template>
<el-input v-bind="mergeProps($attrs, props)" />
</template>
使用 mergeProps($attrs, props)
,可以将所有传入属性(包括原组件支持的)合并传递,无需一一显式声明,同时保留类型提示 ✨。
🎭 场景二:插槽(slots)穿透不翻车
Vue3 的插槽机制相比 Vue2 更加灵活,特别是组合式 API 下的 $slots
和 defineSlots
,让插槽传递和类型定义更加清晰。
为什么插槽要穿透?
- 原组件通常提供了很多插槽能力(如表格 header、input prepend/append),如果封装时不透传,用户无法扩展;
- 插槽透传能够实现更大的复用性;
使用 v-for
动态绑定所有插槽是一个万能方案,适用于绝大多数组件封装场景。
封装组件后,我们仍然希望原组件的插槽功能可以继续使用,特别是具名插槽。
👎 错误做法:
vue
<el-table>
<slot></slot> <!-- 只能透传默认插槽 -->
</el-table>
👍 正确做法(v-for 遍历):
vue
<el-table v-bind="mergeProps($attrs, props)">
<template v-for="(slotFn, name) in $slots" #[name]="slotProps">
<slot :name="name" v-bind="slotProps" />
</template>
</el-table>
这种方式支持透传所有插槽,包含默认插槽和具名插槽(例如 #header
、#footer
等)。
🧪 场景三:暴露内部组件方法
在封装组件时,如果内部组件的方法无法从外部调用,就会降低灵活性。例如:
- 用户希望调用
.focus()
、.validate()
等原组件的方法; - 或者你在封装表单时,希望通过外部控制其校验、重置等逻辑;
Vue3 的 defineExpose()
可以将内部方法暴露给使用 ref
的父组件。
有时我们需要通过 ref 调用封装组件的原始方法,比如聚焦 input、清空表单等。
👎 直接 ref 会拿不到组件实例:
ts
const inputRef = ref()
inputRef.value?.focus() // ❌ undefined
👍 正确暴露方式:
ts
const innerRef = ref()
defineExpose({
focus: () => innerRef.value?.focus(),
clear: () => innerRef.value?.clear(),
})
或者简洁一点:
ts
defineExpose(innerRef)
这样父组件就可以用 ref
调用内部组件的方法,非常实用!
🧠 场景四:类型提示完整才是真的封装!
使用 TypeScript 时,类型安全对组件封装尤为重要。如果封装后类型提示缺失,将会严重影响开发体验。
比如:
- props 不提示或提示错误,导致传参混乱;
- 插槽不提示内容结构,不清楚传入插槽函数应该接收什么参数;
- 事件没有类型定义,容易误传;
这时候就需要用到 Vue 提供的一些 TS 工具类型(如 ExtractPublicPropTypes
, defineSlots
)来增强封装组件的类型能力。
我们希望即使是封装后的组件,开发者依然能享受到 TS 的智能提示(props、slots、events)。
Props 类型提取
ts
import type { ExtractPublicPropTypes } from 'vue'
const inputProps = { ... } // 原组件 props 定义
type InputProps = ExtractPublicPropTypes<typeof inputProps>
这样 defineProps<InputProps>()
就拥有完整的类型推导!
插槽类型定义(Vue 3.3+)
ts
const slots = defineSlots<{
default(props: Record<string, any>): any
prepend(): any
append(): any
}>()
🧾 封装模版参考(Element Plus Input)
vue
<script setup lang="ts">
import { ElInput } from 'element-plus'
import type { InputProps } from './types'
import { mergeProps, ref, defineExpose } from 'vue'
const props = defineProps<InputProps>()
const innerRef = ref()
defineExpose({
focus: () => innerRef.value?.focus()
})
</script>
<template>
<el-input
ref="innerRef"
v-bind="mergeProps($attrs, props)"
>
<template
v-for="(slotFn, name) in $slots"
#[name]="slotProps"
>
<slot :name="name" v-bind="slotProps" />
</template>
</el-input>
</template>
🧩 最佳实践小结
问题 | 推荐方式 | 说明 |
---|---|---|
Props 透传 | mergeProps($attrs, props) |
合并外部传入属性与自定义 props |
插槽传递 | 遍历 $slots 透传 |
支持所有插槽透传 |
方法暴露 | defineExpose(ref) |
暴露子组件方法供外部调用 |
类型补全 | ExtractPublicPropTypes 、defineSlots |
提供完整的 TS 支持 |
🔚 写在最后
组件封装不是复制粘贴,而是理解框架底层能力 + 提炼业务场景的抽象。
掌握好 props、slots、ref、TS 类型的正确使用方式,封装出来的组件才能真正做到"可复用、可维护、开发愉快"。
如果你觉得有收获,记得点赞 + 收藏 ⭐,后续我会继续分享更多 Vue3 实战技巧!