🚀 Vue3 高级组件封装实践指南:让你的组件更优雅、更好用

二次封装不是 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 下的 $slotsdefineSlots,让插槽传递和类型定义更加清晰。

为什么插槽要穿透?

  • 原组件通常提供了很多插槽能力(如表格 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) 暴露子组件方法供外部调用
类型补全 ExtractPublicPropTypesdefineSlots 提供完整的 TS 支持

🔚 写在最后

组件封装不是复制粘贴,而是理解框架底层能力 + 提炼业务场景的抽象

掌握好 props、slots、ref、TS 类型的正确使用方式,封装出来的组件才能真正做到"可复用、可维护、开发愉快"。


如果你觉得有收获,记得点赞 + 收藏 ⭐,后续我会继续分享更多 Vue3 实战技巧!

相关推荐
Dolphin_海豚1 分钟前
vapor 中的 ast 是如何被 transform 到 IR 的
前端·vue.js·源码
袋鱼不重1 小时前
Vue3 Effect源码解析
前端·javascript·vue.js
拾光拾趣录1 小时前
Vue 双向数据绑定原理
前端·vue.js
90后的晨仔1 小时前
vue中的watch 可以监听对象吗?
前端·vue.js
影子信息2 小时前
vue3 组件生命周期,watch和computed
前端·javascript·vue.js
90后的晨仔2 小时前
👂《侦听器(watch)》— 监听数据变化执行副作用逻辑
前端·vue.js
90后的晨仔3 小时前
⚙️ 《响应式原理》— Vue 是怎么做到自动更新的?
前端·vue.js
90后的晨仔3 小时前
🧮《计算属性》— 自动根据其它响应式数据得出结果
前端·vue.js
baozj3 小时前
html2canvas + jspdf 前端PDF分页优化方案:像素级分析解决文字、表格内容截断问题
前端·vue.js·开源
可乐拌花菜3 小时前
Vue3 + Pinia:子组件修改 Pinia 数据,竟然影响了原始数据?
前端·vue.js