Vue3组合式API迁移:如何复用defineProps、defineEmits与defineExpose?
defineProps/defineEmits/defineExpose及组件继承的实现 我正在将一个中型应用从Vue 2迁移至Vue 3,为此将所有代码重构为组合式API(Composition API)。我认可组合式API,但未发现其在简化代码上的明显优势,部分场景下代码反而更复杂冗长,希望是我的误解。
这种情况在重构混入(mixins)时尤为显著:使用mixins时只需引入即可获得全部所需功能,而使用组合式函数(composables)则需了解其内部实现以提取特定功能。
我的核心问题在于如何复用defineProps()、defineEmits()和defineExpose()。我的应用中有一系列输入组件,例如:
- InputText:仅支持文本输入
- InputNumber:复用InputText,限制仅输入数字
- InputMoney:复用InputNumber,添加货币格式化
- InputNumberRange:复用两个InputNumber组件
- 等等
这些输入组件逻辑高度相似,在Vue2中仅需一个InputMixin及少量覆写即可实现复用。但在组合式API下,我不知如何实现如此简洁的复用方式,请问如何在这些组件间复用defineProps()、defineEmits()和defineExpose()?是否可以使用类似extends: MyComponent的继承方式?
解决方案
1. 组件继承:extends在Vue3中仍然有效
Vue 3保留了组件继承能力,你可以像Vue2一样用extends选项复用基础组件的props、emits、方法甚至模板逻辑,这是最贴近Vue2 mixin复用体验的方式。
示例:
<!-- InputText.vue --> <script setup> const props = defineProps({ modelValue: String, placeholder: String, disabled: Boolean }) const emit = defineEmits(['update:modelValue']) const handleInput = (e) => { emit('update:modelValue', e.target.value) } defineExpose({ focus: () => { /* 实现聚焦逻辑 */ } }) </script>
<!-- InputNumber.vue --> <script setup> import InputText from './InputText.vue' // 通过extends继承InputText的所有声明 export default { extends: InputText } // 扩展数字专属props const props = defineProps({ min: Number, max: Number }) const emit = defineEmits(['update:modelValue']) // 覆写InputText的输入处理逻辑,添加数字校验 const handleInput = (e) => { const value = e.target.value.replace(/[^0-9]/g, '') if (props.min !== undefined && Number(value) < props.min) { emit('update:modelValue', props.min.toString()) } else if (props.max !== undefined && Number(value) > props.max) { emit('update:modelValue', props.max.toString()) } else { emit('update:modelValue', value) } } </script>
注意:<script setup>中需通过export default单独声明extends,因为<script setup>是编译时语法,不会直接暴露组件选项。这种方式能快速复用基础组件的所有能力,同时按需覆写或扩展。
2. 封装可复用逻辑:组合式函数+类型工具
如果更偏向组合式API风格,可以将通用的props、emits定义封装成函数,在子组件中导入并扩展,结合Vue3类型系统还能保证类型安全。
步骤1:封装通用props/emits
// composables/useInputCommon.js export const useInputProps = () => ({ modelValue: { type: [String, Number], default: '' }, placeholder: { type: String, default: '' }, disabled: { type: Boolean, default: false } }) export const useInputEmits = () => ['update:modelValue', 'blur', 'focus']
步骤2:在组件中复用并扩展
<!-- InputText.vue --> <script setup> import { useInputProps, useInputEmits } from '../composables/useInputCommon' const props = defineProps(useInputProps()) const emit = defineEmits(useInputEmits()) const handleInput = (e) => { emit('update:modelValue', e.target.value) } const handleBlur = (e) => emit('blur', e) const handleFocus = (e) => emit('focus', e) defineExpose({ focus: () => { /* 实现聚焦 */ } }) </script>
<!-- InputNumber.vue --> <script setup> import { useInputProps, useInputEmits } from '../composables/useInputCommon' // 复用通用props并添加数字专属配置 const props = defineProps({ ...useInputProps(), min: { type: Number, default: -Infinity }, max: { type: Number, default: Infinity } }) const emit = defineEmits(useInputEmits()) const handleInput = (e) => { const rawValue = e.target.value.replace(/[^0-9.-]/g, '') const value = Number(rawValue) || '' if (value >= props.min && value <= props.max) { emit('update:modelValue', value) } } // 封装暴露方法的复用逻辑 const focus = () => { /* 复用或重写聚焦逻辑 */ } defineExpose({ focus }) </script>
若要复用defineExpose内容,可将暴露的方法封装到组合式函数:
// composables/useInputCommon.js export const useInputExpose = (inputRef) => { const focus = () => inputRef.value?.focus() return { focus } }
组件中使用:
<script setup> import { ref } from 'vue' import { useInputExpose } from '../composables/useInputCommon' const inputRef = ref(null) const exposed = useInputExpose(inputRef) defineExpose(exposed) </script>
3. 嵌套复用场景:组件引入+组合式函数
对于InputNumberRange这类需要复用多个子组件的场景,可直接引入组件并通过组合式函数共享逻辑:
<!-- InputNumberRange.vue --> <script setup> import InputNumber from './InputNumber.vue' import { useInputProps } from '../composables/useInputCommon' const props = defineProps({ ...useInputProps(), startValue: Number, endValue: Number }) const emit = defineEmits(['update:startValue', 'update:endValue']) </script> <template> <div class="range-input"> <InputNumber :model-value="startValue" @update:model-value="emit('update:startValue', $event)" :min="props.min" :max="props.max" /> <span>至</span> <InputNumber :model-value="endValue" @update:model-value="emit('update:endValue', $event)" :min="props.min" :max="props.max" /> </div> </template>
组合式API优势补充
你觉得组合式API在部分场景更冗长,可能是初期重构时未找到合适的封装方式。相比mixins,它的核心优势在于:
- 明确的依赖关系:能清晰看到组件用到的复用逻辑,避免mixins的命名冲突和隐式依赖
- 更好的类型支持:配合TypeScript,组合式函数的类型推导更清晰,而mixins的类型处理复杂
- 灵活的逻辑组合:可按需组合多个小的组合式函数,而非像mixins一样一次性引入所有逻辑
对于你的输入组件系列,推荐先尝试extends继承方式快速迁移现有逻辑,之后再逐步将通用逻辑拆解为组合式函数,实现更细粒度的复用。
内容的提问来源于stack exchange,提问作者Zalo

