心态好好
摆脱懒惰和拖延(๑>◡<๑)

如何写Vue3框架组件封装

在 Vue3 开发封装按钮组件时,把主色、圆角、内边距直接写在模板和逻辑里;封装卡片时,宽高、阴影、间距随手写死;封装弹窗时,默认宽高、关闭方式各处不一致。某天要改按钮默认大小,或全项目卡片统一加圆角,就得全局翻找组件代码一个个改;
解决思路和 CSS 统一样式变量异曲同工:把组件的通用配置抽离成可统一管理的参数,定义合理默认值,支持全局配置 + 局部覆盖,结合类型校验让配置更规范。Vue3 本身提供了 Props、Provide/Inject、TS 类型约束等原生能力,不用依赖第三方库,就能优雅实现组件的多配置项设计,让组件封装更灵活、更易维护、更适合团队协作。
以下一步步写出完善配置项的 Vue3 组件,风格统一、拓展灵活,可以复用这套思路。

一、Vue3 可配置组件的核心设计准则

在写配置项之前,先定好设计原则,避免后续封装混乱,这和定义 CSS 全局变量前定命名规范是一个道理,核心遵循 5 点:
  1. 单一职责:一个组件只做一件事,配置项只围绕核心功能设计,不贪多求全;
  2. 配置归一:所有可定制的属性都收敛到 Props / 全局配置,不分散在模板 / 逻辑的硬编码中;
  3. 合理默认:给所有配置项设通用默认值,实现「开箱即用」,不用每次使用都传参;
  4. 灵活覆盖:支持局部 Props 覆盖(单个组件定制)+全局配置覆盖(全项目统一),优先级清晰;
  5. 类型友好:结合 TypeScript 做配置项类型约束,实现编辑器智能提示,告别传错参数、查文档的麻烦。
遵循这几点,写出来的配置项组件,既能单独使用又能全局统一,开发和维护效率都会大幅提升。

二、优雅设计配置项

以最常用的Button 按钮组件为示例,从「基础硬编码改造」到「组件库级全局配置」,一步步实现可配置化,每一步都能直接落地。

环境准备

Vue3 单文件组件(SFC),推荐结合 Vite 开发(自带 SCSS 编译,可直接用之前的 CSS 全局变量),后续示例会兼顾原生 Vue3(JS)Vue3+TS(主流方案)。
先准备好全局样式(呼应之前的 CSS 文章),在src/styles/global.scss中定义 CSS 变量 + Sass 变量,组件样式直接复用,避免样式和配置项脱节:
// src/styles/global.scss
// 1. 定义Sass变量
$color-primary: #4285F4;
$color-secondary: #FFC107;
$color-success: #34A853;
$gap-8: 8px;
$gap-16: 16px;
$radius-base: 8px;
$radius-circle: 50%;

// 2. 注入CSS全局变量
:root {
  --color-primary: #{$color-primary};
  --color-secondary: #{$color-secondary};
  --color-success: #{$color-success};
  --gap-8: #{$gap-8};
  --gap-16: #{$gap-16};
  --radius-base: #{$radius-base};
  --radius-circle: #{$radius-circle};
}

// 3. 通用样式重置
* {
  margin: 0;
  padding: 0;
  box-sizing: border-box;
}
button {
  border: none;
  outline: none;
  cursor: pointer;
  background: transparent;
}
main.js/ts中全局引入:
// src/main.ts
import { createApp } from 'vue'
import App from './App.vue'
import './styles/global.scss'

createApp(App).mount('#app')

第一步:Props 定义核心配置,带默认值 + 类型校验

这是 Vue3 组件配置化的基础操作,解决最核心的「硬编码问题」:把组件中可定制的属性(如按钮类型、大小、圆角、禁用状态)通过defineProps定义为 Props,给每个 Props 设默认值类型校验,模板中直接复用 Props。
创建src/components/MyButton.vue,实现基础可配置按钮:
<template>
  <button
    class="my-button"
    :class="[
      `my-button--${type}`,
      `my-button--${size}`,
      { 'my-button--round': round, 'my-button--disabled': disabled }
    ]"
    :disabled="disabled"
    @click="$emit('click', $event)"
  >
    <slot></slot>
  </button>
</template>

<script setup>
// 基础Props定义:类型校验+默认值
const props = defineProps({
  // 按钮类型:主色/辅助色/成功色
  type: {
    type: String,
    default: 'primary', // 默认主色
    validator: (val) => ['primary', 'secondary', 'success'].includes(val) // 自定义校验,避免传错值
  },
  // 按钮大小:小/基础/大
  size: {
    type: String,
    default: 'base',
    validator: (val) => ['sm', 'base', 'lg'].includes(val)
  },
  // 是否圆角
  round: {
    type: Boolean,
    default: false
  },
  // 是否禁用
  disabled: {
    type: Boolean,
    default: false
  }
})

// 暴露点击事件
defineEmits(['click'])
</script>

<style scoped lang="scss">
.my-button {
  padding: var(--gap-8) var(--gap-16);
  border-radius: var(--radius-base);
  font-size: 16px;
  transition: all 0.2s ease;

  // 按类型区分颜色(复用CSS全局变量)
  &--primary {
    background: var(--color-primary);
    color: #fff;
  }
  &--secondary {
    background: var(--color-secondary);
    color: #333;
  }
  &--success {
    background: var(--color-success);
    color: #fff;
  }

  // 按大小区分内边距/字体
  &--sm {
    padding: calc(var(--gap-8)/2) var(--gap-8);
    font-size: 14px;
  }
  &--lg {
    padding: var(--gap-8) var(--gap-16)*1.5;
    font-size: 18px;
  }

  // 圆角样式
  &--round {
    border-radius: var(--radius-circle);
  }

  // 禁用样式
  &--disabled {
    opacity: 0.6;
    cursor: not-allowed;
  }

  // hover效果(非禁用时)
  &:not(.my-button--disabled):hover {
    filter: brightness(0.9);
  }
}
</style>
使用方式(开箱即用,需定制时传参):
<template>
  <!-- 默认按钮:primary+base+非圆角+未禁用 -->
  <MyButton @click="handleClick">默认按钮</MyButton>
  <!-- 定制按钮:secondary+sm+圆角 -->
  <MyButton type="secondary" size="sm" round>定制按钮</MyButton>
  <!-- 禁用按钮:success+lg -->
  <MyButton type="success" size="lg" disabled>禁用按钮</MyButton>
</template>

<script setup>
import MyButton from './components/MyButton.vue'
const handleClick = () => console.log('按钮点击')
</script>

第二步: 抽离默认配置对象,统一维护配置项

当组件配置项越来越多(比如新增loadingiconplain等属性),如果把所有默认值都写在defineProps的配置里,会导致代码臃肿、配置分散,维护起来需要翻找大量代码 —— 这和 CSS 中魔法数值散落在各个文件的问题一样。
解决方法:抽离默认配置对象,把所有 Props 的默认值收敛到一个对象中,defineProps直接引用该对象的属性,就像把 CSS 魔法数值收敛到:root中一样。
改造MyButton.vue的脚本部分,抽离defaultProps
<script setup>
// 1. 抽离默认配置对象:所有配置项的默认值统一维护
const defaultProps = {
  type: 'primary',
  size: 'base',
  round: false,
  disabled: false,
  // 新增配置项,无需修改Props结构,直接加在这就行
  loading: false,
  plain: false
}

// 2. Props引用默认配置,类型校验+自定义校验
const props = defineProps({
  type: {
    type: String,
    default: defaultProps.type,
    validator: (val) => ['primary', 'secondary', 'success'].includes(val)
  },
  size: {
    type: String,
    default: defaultProps.size,
    validator: (val) => ['sm', 'base', 'lg'].includes(val)
  },
  round: {
    type: Boolean,
    default: defaultProps.round
  },
  disabled: {
    type: Boolean,
    default: defaultProps.disabled
  },
  loading: {
    type: Boolean,
    default: defaultProps.loading
  },
  plain: {
    type: Boolean,
    default: defaultProps.plain
  }
})

defineEmits(['click'])
</script>
所有配置项的默认值都在defaultProps中统一管理,后期新增 / 修改默认值,只需要改这一个对象,不用在defineProps中逐个翻找,维护效率大幅提升。

第三步: 全局配置 + 局部覆盖,实现组件库级灵活度

在实际项目中,可能要求所有按钮默认类型是secondary,默认大小是sm,如果每个使用MyButton的地方都手动传type="secondary" size="sm",会非常繁琐 —— 这和 CSS 中全局变量统一样式,局部可覆盖的需求高度契合。
Vue3 提供Provide/Inject 原生能力,可实现组件全局配置:在main.js/ts中通过app.provide注入全局配置,组件内部通过inject接收,然后定义配置优先级局部 Props > 全局配置 > 默认配置,完美实现「全项目统一 + 单个组件定制」。

1:封装全局配置插件,注入全局配置

创建src/components/MyButton/config.js,抽离按钮的全局配置键名和默认全局配置:
// 全局配置的唯一键名(避免和其他组件冲突)
export const BUTTON_PROVIDE_KEY = Symbol('MY_BUTTON_GLOBAL_CONFIG')

// 按钮默认全局配置(和组件默认配置一致,可按需修改)
export const defaultGlobalConfig = {
  type: 'primary',
  size: 'base',
  round: false,
  plain: false
}
main.js/ts中注入全局配置(可根据项目需求自定义):
// src/main.ts
import { createApp } from 'vue'
import App from './App.vue'
import './styles/global.scss'
import MyButton from './components/MyButton.vue'
import { BUTTON_PROVIDE_KEY } from './components/MyButton/config'

const app = createApp(App)

// 全局注册按钮组件(可选,也可局部引入)
app.component('MyButton', MyButton)

// 注入按钮全局配置:所有按钮默认类型secondary,大小sm(项目定制)
app.provide(BUTTON_PROVIDE_KEY, {
  type: 'secondary',
  size: 'sm'
})

app.mount('#app')

 2:组件内部接收全局配置,实现配置合并

MyButton.vue,注入全局配置,封装配置合并方法,按优先级生成最终配置:
<template>
  <!-- 模板中使用合并后的最终配置 -->
  <button
    class="my-button"
    :class="[
      `my-button--${finalConfig.type}`,
      `my-button--${finalConfig.size}`,
      { 
        'my-button--round': finalConfig.round, 
        'my-button--disabled': finalConfig.disabled,
        'my-button--loading': finalConfig.loading,
        'my-button--plain': finalConfig.plain
      }
    ]"
    :disabled="finalConfig.disabled || finalConfig.loading"
    @click="$emit('click', $event)"
  >
    <span v-if="finalConfig.loading">🔄</span>
    <slot></slot>
  </button>
</template>

<script setup>
import { inject, computed } from 'vue'
import { BUTTON_PROVIDE_KEY, defaultGlobalConfig } from './config'

// 1. 组件默认配置
const defaultProps = {
  type: 'primary',
  size: 'base',
  round: false,
  disabled: false,
  loading: false,
  plain: false
}

// 2. Props定义
const props = defineProps({
  type: { type: String, validator: (val) => ['primary', 'secondary', 'success'].includes(val) },
  size: { type: String, validator: (val) => ['sm', 'base', 'lg'].includes(val) },
  round: { type: Boolean },
  disabled: { type: Boolean },
  loading: { type: Boolean },
  plain: { type: Boolean }
})

// 3. 注入全局配置(如果没有,用默认全局配置)
const globalConfig = inject(BUTTON_PROVIDE_KEY, defaultGlobalConfig)

// 4. 配置合并:优先级 Props > 全局配置 > 组件默认配置
const finalConfig = computed(() => {
  return {
    ...defaultProps, // 最底层:组件默认配置
    ...globalConfig, // 中间层:全局配置
    ...props // 最顶层:局部Props(覆盖所有)
  }
})

defineEmits(['click'])
</script>

3:补充新增配置的样式(loading+plain)

<style scoped lang="scss">
// ... 原有样式不变

// 镂空样式
&--plain {
  background: transparent;
  border: 1px solid currentColor;
  color: var(--color-primary);
  &--secondary {
    color: var(--color-secondary);
  }
  &--success {
    color: var(--color-success);
  }
}

// 加载中样式
&--loading {
  opacity: 0.8;
  cursor: not-allowed;
  span {
    margin-right: var(--gap-8);
  }
}
</style>
全局配置生效,局部可覆盖:
<template>
  <!-- 全局配置生效:secondary+sm+非圆角 -->
  <MyButton>全局默认按钮</MyButton>
  <!-- 局部覆盖:primary+lg+圆角(优先级最高) -->
  <MyButton type="primary" size="lg" round>局部覆盖按钮</MyButton>
  <!-- 新增配置:loading+plain -->
  <MyButton loading plain>加载中镂空按钮</MyButton>
</template>
实现一次配置,全项目生效,后期需要修改全项目按钮的默认样式,只需要改main.ts中的全局配置,不用逐个组件修改;同时保留局部 Props 覆盖能力,满足特殊模块的定制需求。

三、配套技巧:让配置项更优雅、更实用

和 Sass 的嵌套、混合器能让 CSS 更高效一样,Vue3 组件配置也有一些配套小技巧,能让配置项的设计更贴合实际开发,兼顾灵活性和易用性,这些技巧也是主流组件库的通用设计思路:

1. 配置项语义化命名,和 CSS 变量呼应

命名遵循 **前缀 + 语义** 原则,和之前的 CSS 全局变量命名风格统一,一眼就能看懂含义,避免无意义的命名:

类型:type(取值primary/secondary/success,和 CSS 颜色变量--color-primary呼应);

大小:size(取值sm/base/lg,和 CSS 间距变量--gap-sm呼应);

布尔型配置:直接用语义化单词(round/loading/disabled/plain),不用isRound/isLoading(Vue 支持布尔型 Props 简写)。

2. 布尔型配置简写,简化使用

Vue 中布尔型 Props 支持简写方式,不用写round="true",直接写round即可,大幅简化组件使用:
<!-- 推荐:简写 -->
<MyButton round loading></MyButton>
<!-- 不推荐:完整写法 -->
<MyButton round="true" loading="true"></MyButton>

3. 复杂配置用「对象 / 数组」,替代多个零散 Props

当组件有复杂定制需求时(如弹窗的底部按钮、卡片的操作栏),不要定义多个零散的 Props(如btn1Text/btn1Click/btn2Text/btn2Click),而是用数组 / 对象传参,更灵活、更易拓展:
// 弹窗组件的底部按钮配置:用数组传参,支持无限拓展
<MyModal :footer-buttons="footerButtons"></MyModal>

<script setup>
const footerButtons = [
  { text: '取消', type: 'secondary', click: () => console.log('取消') },
  { text: '确认', type: 'primary', click: () => console.log('确认'), round: true }
]
</script>

4. 配置项联动处理,实现一键生效

让相关配置项自动联动,提升易用性:

例 1:loadingtrue时,自动将disabled设为true,不用手动传disabled

例 2:plaintrue时,自动修改按钮的背景和边框,不用手动写样式;

例 3:typeprimary时,自动将plain样式的文字颜色设为--color-primary

5. 暴露方法 / 插槽,让配置更灵活

配置项不是万能的,对于自定义的场景,结合「插槽 + 暴露方法」能让组件更灵活,避免定义过多无用的配置项:

插槽:用于定制组件内部内容(如按钮的图标、卡片的标题);

暴露方法:用于组件的动态操作(如弹窗的open/close方法、抽屉的toggle方法)。

例:给按钮添加图标插槽,支持定制图标:
<template>
  <button ...>
    <slot name="icon"></slot> <!-- 图标插槽 -->
    <span v-if="finalConfig.loading">🔄</span>
    <slot></slot> <!-- 默认插槽 -->
  </button>
</template>

<!-- 使用 -->
<MyButton>
  <template #icon>🔍</template>
  搜索按钮
</MyButton>

四、案例:封装可配置的 Card 卡片组件

结合上面,封装一个项目中最常用的Card 卡片组件,包含Props 配置 + 默认配置 + 全局配置 + TS 类型 + 插槽 + 配置联动,可直接复用。
支持配置:标题、阴影、内边距、圆角、边框、是否悬浮高亮;实现:全局配置默认样式,局部可覆盖,插槽定制标题 / 内容,配置联动(悬浮高亮自动加阴影)。

1. 配置文件:src/components/MyCard/config.ts

import { InjectionKey } from 'vue'

// 阴影类型联合类型
export type CardShadow = 'light' | 'base' | 'heavy' | 'none'
// 卡片配置接口
export interface CardConfig {
  title: string
  shadow: CardShadow
  padding: string
  radius: string
  border: boolean
  hoverable: boolean
}
// 全局注入键
export const CARD_PROVIDE_KEY: InjectionKey<Partial<CardConfig>> = Symbol('MY_CARD_GLOBAL_CONFIG')
// 默认全局配置
export const defaultGlobalConfig: CardConfig = {
  title: '',
  shadow: 'base',
  padding: 'var(--gap-24)',
  radius: 'var(--radius-lg)',
  border: false,
  hoverable: true
}

2. 组件文件:src/components/MyCard.vue

<template>
  <div class="my-card" :class="cardClass" :style="cardStyle">
    <!-- 标题区域:插槽优先,无插槽则用配置的title -->
    <div class="my-card__header" v-if="finalConfig.title || $slots.header">
      <slot name="header">{{ finalConfig.title }}</slot>
    </div>
    <!-- 内容区域 -->
    <div class="my-card__content">
      <slot></slot>
    </div>
  </div>
</template>

<script setup lang="ts">
import { inject, computed } from 'vue'
import { CardConfig, CardShadow, CARD_PROVIDE_KEY, defaultGlobalConfig } from './config'

// 组件默认配置
const defaultProps: CardConfig = {
  title: '',
  shadow: 'base',
  padding: 'var(--gap-24)',
  radius: 'var(--radius-lg)',
  border: false,
  hoverable: true
}

// TS版Props
const props = defineProps<{
  title?: string
  shadow?: CardShadow
  padding?: string
  radius?: string
  border?: boolean
  hoverable?: boolean
}>()

// 注入全局配置
const globalConfig = inject<Partial<CardConfig> | undefined>(CARD_PROVIDE_KEY, defaultGlobalConfig)

// 合并配置
const finalConfig = computed<CardConfig>(() => ({
  ...defaultProps,
  ...globalConfig,
  ...props
}))

// 动态class:配置联动(hoverable为true时,加hover样式;border为true时加边框)
const cardClass = computed(() => ({
  'my-card--border': finalConfig.value.border,
  'my-card--hoverable': finalConfig.value.hoverable
}))

// 动态style:阴影、内边距、圆角(复用CSS全局变量)
const cardStyle = computed(() => ({
  boxShadow: finalConfig.value.shadow === 'none' ? 'none' : `var(--shadow-${finalConfig.value.shadow})`,
  padding: finalConfig.value.padding,
  borderRadius: finalConfig.value.radius
}))
</script>

<style scoped lang="scss">
.my-card {
  background: var(--color-card);
  transition: all 0.2s ease;
  width: 100%;

  &__header {
    padding-bottom: var(--gap-16);
    font-size: var(--font-size-lg);
    font-weight: 700;
    color: var(--color-text-1);
    border-bottom: 1px solid #eee;
    margin-bottom: var(--gap-16);
  }

  &__content {
    color: var(--color-text-2);
    line-height: 1.6;
  }

  // 边框样式
  &--border {
    border: 1px solid #eee;
  }

  // 悬浮样式:配置联动,hoverable为true时生效
  &--hoverable {
    &:hover {
      box-shadow: var(--shadow-heavy) !important;
      transform: translateY(-2px);
    }
  }
}
</style>

3. 全局配置(main.ts)

// 注入卡片全局配置:默认无阴影、有边框、内边距缩小
app.provide(CARD_PROVIDE_KEY, {
  shadow: 'none',
  border: true,
  padding: 'var(--gap-16)'
} as Partial<CardConfig>)

4. 使用方式

<template>
  <div style="max-width: 800px; margin: var(--gap-24) auto; padding: 0 var(--gap-16)">
    <!-- 全局配置生效:无阴影+有边框+小内边距 -->
    <MyCard title="全局默认卡片">
      这是全局配置的默认卡片,无阴影、有边框、内边距16px,hover时自动加阴影并上移。
    </MyCard>

    <!-- 局部覆盖:重阴影+无边框+大圆角+自定义标题 -->
    <MyCard 
      shadow="heavy" 
      :border="false" 
      radius="var(--radius-base)"
      style="margin-top: var(--gap-24)"
    >
      <template #header>
        <span style="color: var(--color-primary)">局部定制卡片</span>
      </template>
      这是局部覆盖的卡片,重阴影、无边框、圆角8px,hover样式依然生效。
    </MyCard>

    <!-- 禁用悬浮:hoverable=false -->
    <MyCard 
      title="禁用悬浮卡片" 
      :hoverable="false"
      style="margin-top: var(--gap-24)"
    >
      这个卡片禁用了悬浮效果,hover时不会有阴影和位移。
    </MyCard>
  </div>
</template>

<script setup lang="ts">
import MyCard from './components/MyCard.vue'
</script>

为什么要优雅设计组件配置项?

核心价值有 5 点:
  1. 告别硬编码魔法配置:所有可定制属性都收敛到配置项中,统一维护,后期修改需求不用全局翻找代码;
  2. 组件复用性拉满:一处封装,多处使用,全局配置实现全项目统一,局部 Props 支持定制,兼顾通用性和灵活性;
  3. 团队协作更规范:配置项的类型、命名、默认值统一,避免各人写各人的风格,减少沟通和联调成本;
  4. 开发体验大幅提升:TS 类型约束实现智能提示,不用查文档,不用记配置,降低出错概率;
  5. 项目可维护性提升:新接手项目的开发人员,通过配置项就能快速理解组件的使用方式,不用逐行读组件代码。
赞(0) 打赏
未经允许不得转载:东东的小屋 » 如何写Vue3框架组件封装

评论 抢沙发

觉得文章有用就打赏一下文章作者

非常感谢你的打赏,我们将继续提供更多优质内容,让我们一起创建更加美好的网络世界!

支付宝扫一扫

微信扫一扫

登录

找回密码

注册