Vue使用extend继承组件时,如何访问带类型的Emits及复用资源?
Vue3 实现组件复用:替代 Vue2 extend 方案
我有两个功能高度相似的搜索组件,它们共享相同的 props、refs、watchers 和 emitter。目前通过 defineComponent 继承基础组件后,仅能访问 props,但带类型的 emits、基础组件的 ref 和 watcher 都无法正确复用,希望复刻 Vue2 的 extend 功能来减少重复代码。
基础组件代码(原版本)
<script setup lang="ts"> import { ref, watch } from 'vue' import type { OptionType } from './BaseSearch.types' export interface SearchEmits { (event: 'update:modelValue', val: OptionType[]): void (event: 'input', val: string): void } const emit = defineEmits<SearchEmits>() const props = withDefaults( defineProps<{ closeOnSelect?: boolean disabled?: boolean errorMessage?: string isError?: boolean isLoading?: boolean label?: string modelValue: OptionType[] options: OptionType[] placeholder?: string required?: boolean searchable?: boolean }>(), { required: false, searchable: true, } ) const input = ref('') watch(input, () => { emit('input', input.value) }) </script>
继承组件代码(原版本)
<template> <!-- 自定义模板已省略 --> </template> <script lang="ts"> import { defineComponent, PropType, ref, watch } from 'vue' import MultiSelect from 'MultiSelect' import BaseSearch, { SearchEmits } from '../BaseSearch/BaseSearch.vue' export default defineComponent({ components: { MultiSelect }, extends: BaseSearch, props: { // 扩展新props canClear: { required: false, type: Boolean, }, }, setup(props, ctx) { const emit: SearchEmits = ctx.emit // 期望直接复用基础组件的input ref const input = ref('') // 期望直接复用基础组件的watcher watch(input, () => { emit('input', input.value) }) return { emit, highlightMatchedTerm, input, props } }, }) </script>
解决方案
方案一:通过 defineExpose 配合 extends 实现继承复用
Vue3 的 <script setup> 默认是封闭作用域,内部变量无法被外部访问,需要手动通过 defineExpose 暴露需要复用的资源。
1. 修改基础组件,暴露资源并封装复用逻辑
<script setup lang="ts"> import { ref, watch } from 'vue' import type { OptionType } from './BaseSearch.types' export interface SearchEmits { (event: 'update:modelValue', val: OptionType[]): void (event: 'input', val: string): void } // 导出props类型方便子组件复用 export interface BaseSearchProps { closeOnSelect?: boolean disabled?: boolean errorMessage?: string isError?: boolean isLoading?: boolean label?: string modelValue: OptionType[] options: OptionType[] placeholder?: string required?: boolean searchable?: boolean } const emit = defineEmits<SearchEmits>() const props = withDefaults(defineProps<BaseSearchProps>(), { required: false, searchable: true, }) const input = ref('') // 将watch逻辑封装为可复用函数 export const setupInputWatcher = (inputRef: typeof input, emitInstance: typeof emit) => { return watch(inputRef, () => { emitInstance('input', inputRef.value) }) } // 执行默认watch逻辑 setupInputWatcher(input, emit) // 暴露input ref给子组件访问 defineExpose({ input, }) </script>
2. 修改继承组件,复用基础资源
<template> <!-- 自定义模板 --> </template> <script lang="ts"> import { defineComponent, getCurrentInstance } from 'vue' import MultiSelect from 'MultiSelect' import BaseSearch, { SearchEmits, BaseSearchProps, setupInputWatcher } from '../BaseSearch/BaseSearch.vue' export default defineComponent({ components: { MultiSelect }, extends: BaseSearch, props: { // 扩展新props canClear: { required: false, type: Boolean, }, // 复用基础组件的props类型 ...BaseSearchProps, }, setup(props, ctx) { // 给emit添加类型断言 const emit = ctx.emit as SearchEmits // 获取基础组件实例,访问暴露的input ref const baseInstance = getCurrentInstance()?.parent const input = baseInstance?.exposed?.input // 复用基础组件的watch逻辑 if (input) { setupInputWatcher(input, emit) } // 自定义业务逻辑 const highlightMatchedTerm = () => { // 你的自定义实现 } return { emit, highlightMatchedTerm, input, props } }, }) </script>
方案二:使用组合式函数(Composables)替代继承(推荐)
Vue3 更推荐用组合式 API 封装复用逻辑,相比继承更灵活,也更符合框架设计理念。
1. 创建组合式函数 useSearch.ts
import { ref, watch } from 'vue' import type { OptionType } from './BaseSearch.types' export interface SearchEmits { (event: 'update:modelValue', val: OptionType[]): void (event: 'input', val: string): void } export interface SearchProps { closeOnSelect?: boolean disabled?: boolean errorMessage?: string isError?: boolean isLoading?: boolean label?: string modelValue: OptionType[] options: OptionType[] placeholder?: string required?: boolean searchable?: boolean } export function useSearch(props: SearchProps, emit: SearchEmits) { const input = ref('') watch(input, () => { emit('input', input.value) }) return { input, } }
2. 基础组件使用组合式函数
<script setup lang="ts"> import { withDefaults, defineProps, defineEmits } from 'vue' import { useSearch, SearchProps, SearchEmits } from './useSearch' const props = withDefaults(defineProps<SearchProps>(), { required: false, searchable: true, }) const emit = defineEmits<SearchEmits>() // 复用逻辑 const { input } = useSearch(props, emit) </script>
3. 子组件扩展并复用逻辑
<template> <!-- 自定义模板 --> </template> <script setup lang="ts"> import { withDefaults, defineProps, defineEmits } from 'vue' import MultiSelect from 'MultiSelect' import { useSearch, SearchProps, SearchEmits } from '../useSearch' // 扩展基础props类型 interface ExtendedSearchProps extends SearchProps { canClear?: boolean } const props = withDefaults(defineProps<ExtendedSearchProps>(), { canClear: false, // 继承基础props的默认值 required: false, searchable: true, }) const emit = defineEmits<SearchEmits>() // 复用核心逻辑 const { input } = useSearch(props, emit) // 自定义业务逻辑 const highlightMatchedTerm = () => { // 你的自定义实现 } </script>
说明
- 方案一通过
defineExpose打破<script setup>的封闭性,配合extends实现类似 Vue2 的继承效果,但需要注意实例访问的稳定性。 - 方案二是 Vue3 官方推荐的复用方式,组合式函数可以灵活地在多个组件中共享逻辑,避免继承带来的耦合问题。
内容的提问来源于stack exchange,提问作者DumbDevGirl42069
相关产品推荐
相关产品推荐

