在 Vue 3 中二次封装组件是提高代码复用性和维护性的重要手段。以下是详细的封装方法和最佳实践:
一、封装原则
- 功能扩展:在原有组件基础上添加新功能
- 定制样式:统一项目的 UI 设计规范
- 简化接口:隐藏复杂逻辑,提供简洁的 API
- 复用逻辑:封装可复用的业务逻辑
二、基础封装方法
1. 属性透传(Props)
<script setup>
import BaseButton from './BaseButton.vue'const props = defineProps({// 扩展原生属性size: {type: String,default: 'medium'},// 新增自定义属性loading: Boolean
})
</script><template><BaseButtonv-bind="$attrs" // 透传所有未声明的属性:class="[`btn-size-${size}`,{ 'is-loading': loading }]"><slot /> <!-- 插槽透传 --></BaseButton>
</template>
2. 事件透传
<script setup>
const emit = defineEmits(['click', 'custom'])function handleClick(e) {// 执行扩展逻辑console.log('扩展的点击处理')// 透传原始事件emit('click', e)// 触发自定义事件if (e.target.value) {emit('custom', e.target.value)}
}
</script><template><BaseInput @click="handleClick"@focus="$emit('focus', $event)" // 直接透传/>
</template>
三、高级封装技巧
1. 插槽透传(包含作用域插槽)
<template><BaseTable><!-- 默认插槽 --><slot /><!-- 具名插槽透传 --><template v-for="(_, slotName) in $slots" #[slotName]="scope"><slot :name="slotName" v-bind="scope || {}"/></template><!-- 扩展功能插槽 --><template #actions="scope"><div class="custom-actions"><slot name="custom-actions" v-bind="scope"/><button @click="handleExtraAction">+</button></div></template></BaseTable>
</template>
2. 方法暴露(使用 defineExpose)
<script setup>
import { ref } from 'vue'const innerComponent = ref(null)// 封装内部组件的 focus 方法
function focus() {innerComponent.value?.focus()
}// 暴露给父组件的方法
defineExpose({focus,customMethod: () => console.log('自定义方法')
})
</script><template><BaseInput ref="innerComponent" />
</template>
3. 全局配置注入
<script setup>
import { inject } from 'vue'// 注入全局配置
const globalConfig = inject('formConfig', {labelWidth: '120px',validateOnChange: true
})const props = defineProps({// 允许组件级覆盖全局配置labelWidth: String
})// 最终使用的配置
const actualLabelWidth = computed(() => props.labelWidth || globalConfig.labelWidth
)
</script>
四、最佳实践
1. 支持 TypeScript 类型
<script setup lang="ts">
import type { BaseButtonProps } from './BaseButton.vue'interface ExtendedProps {// 新增属性loading?: boolean// 继承基础组件的类型color?: 'primary' | 'success' | 'warning'
}// 合并原始属性和扩展属性
defineProps<BaseButtonProps & ExtendedProps>()
</script>
2. 默认值处理(合并策略)
<script setup>
import { computed } from 'vue'const props = defineProps({modelValue: { type: [String, Number], default: '' },config: {type: Object,default: () => ({maxLength: 100,showCounter: true})}
})const mergedConfig = computed(() => ({maxLength: 200, // 覆盖默认值showCounter: props.config.showCounter, // 继承配置trimOnBlur: true // 新增功能
}))
</script>
3. 样式封装(Scoped CSS)
<style scoped>
/* 深度选择器修改子组件样式 */
:deep(.base-input__inner) {border-radius: 8px;
}/* 封装通用样式类 */
.btn.primary {background: linear-gradient(to right, #ff8a00, #da1b60);
}
</style>
五、完整示例:封装一个增强型 Input
<template><div class="enhanced-input"><label v-if="label">{{ label }}</label><BaseInput:modelValue="innerValue"v-bind="filteredAttrs":class="{ 'has-error': errorMessage }"@update:modelValue="handleChange"><template #prefix><slot name="prefix" /></template></BaseInput><div v-if="showCounter" class="counter">{{ valueLength }}/{{ maxLength }}</div><div v-if="errorMessage" class="error">{{ errorMessage }}</div></div>
</template><script setup>
import { computed, ref, watch, useAttrs } from 'vue'const props = defineProps({modelValue: [String, Number],label: String,maxLength: {type: Number,default: 100},rules: Array // 校验规则
})const emit = defineEmits(['update:modelValue', 'change'])const attrs = useAttrs()
const innerValue = ref(props.modelValue)
const errorMessage = ref('')// 过滤不需要透传到 BaseInput 的属性
const filteredAttrs = computed(() => {const { label, rules, ...rest } = attrsreturn rest
})// 值处理
const valueLength = computed(() => innerValue.value?.toString().length || 0)
const showCounter = computed(() => props.maxLength > 0)// 值变化处理
function handleChange(value) {innerValue.value = valueemit('update:modelValue', value)emit('change', value)validate(value)
}// 校验逻辑
function validate(value) {if (!props.rules) returnconst rule = props.rules.find(rule => {if (rule.required && !value) return trueif (rule.pattern && !rule.pattern.test(value)) return true// 可扩展更多规则...})errorMessage.value = rule ? rule.message : ''
}// 暴露验证方法
defineExpose({ validate })
</script>
六、封装建议
- 保持接口简单:避免暴露过多内部细节
- 遵循约定大于配置:提供合理的默认值
- 文档注释:使用 JSDoc 说明组件用法
- 可组合性:将复杂组件拆分为多个小组件
- 错误处理:添加边界情况处理
- 类型安全:为 TypeScript 项目提供类型定义
通过合理封装,可以显著提高开发效率和代码质量。建议根据实际项目需求选择合适级别的封装,避免过度封装导致的维护成本增加。