在 Vue3 开发封装按钮组件时,把主色、圆角、内边距直接写在模板和逻辑里;封装卡片时,宽高、阴影、间距随手写死;封装弹窗时,默认宽高、关闭方式各处不一致。某天要改按钮默认大小,或全项目卡片统一加圆角,就得全局翻找组件代码一个个改;
解决思路和 CSS 统一样式变量异曲同工:把组件的通用配置抽离成可统一管理的参数,定义合理默认值,支持全局配置 + 局部覆盖,结合类型校验让配置更规范。Vue3 本身提供了 Props、Provide/Inject、TS 类型约束等原生能力,不用依赖第三方库,就能优雅实现组件的多配置项设计,让组件封装更灵活、更易维护、更适合团队协作。
以下一步步写出完善配置项的 Vue3 组件,风格统一、拓展灵活,可以复用这套思路。
一、Vue3 可配置组件的核心设计准则
在写配置项之前,先定好设计原则,避免后续封装混乱,这和定义 CSS 全局变量前定命名规范是一个道理,核心遵循 5 点:
- 单一职责:一个组件只做一件事,配置项只围绕核心功能设计,不贪多求全;
- 配置归一:所有可定制的属性都收敛到 Props / 全局配置,不分散在模板 / 逻辑的硬编码中;
- 合理默认:给所有配置项设通用默认值,实现「开箱即用」,不用每次使用都传参;
- 灵活覆盖:支持局部 Props 覆盖(单个组件定制)+全局配置覆盖(全项目统一),优先级清晰;
- 类型友好:结合 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>
第二步: 抽离默认配置对象,统一维护配置项
当组件配置项越来越多(比如新增
loading、icon、plain等属性),如果把所有默认值都写在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:loading为true时,自动将disabled设为true,不用手动传disabled;
例 2:plain为true时,自动修改按钮的背景和边框,不用手动写样式;
例 3:type为primary时,自动将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 点:
- 告别硬编码魔法配置:所有可定制属性都收敛到配置项中,统一维护,后期修改需求不用全局翻找代码;
- 组件复用性拉满:一处封装,多处使用,全局配置实现全项目统一,局部 Props 支持定制,兼顾通用性和灵活性;
- 团队协作更规范:配置项的类型、命名、默认值统一,避免各人写各人的风格,减少沟通和联调成本;
- 开发体验大幅提升:TS 类型约束实现智能提示,不用查文档,不用记配置,降低出错概率;
- 项目可维护性提升:新接手项目的开发人员,通过配置项就能快速理解组件的使用方式,不用逐行读组件代码。













