Code前端首页关于Code前端联系我们

Vue3动态组件怎么用才不踩坑?缓存、传参、类型全搞定

terry 52分钟前 阅读数 26 #Vue
文章标签 Vue3动态组件

经常有人说Vue3的动态组件是个“省代码神器”,但用起来要么组件白刷新要么传参丢了要么TypeScript报错一堆警告,其实只要把核心逻辑摸透,再避开几个常见的新手/老手混用陷阱,它真的能帮你搞定很多多标签页、步骤条这种需要频繁切换视图的场景,今天就从最基础的用法讲起,一步步解决大家常遇到的问题。

Vue3动态组件的核心语法到底是什么?

别小看这个基础,很多踩坑都是从乱用标签或者忘加必要属性开始的,Vue3里的动态组件只有一个核心标签:<component :is="组件变量/组件名字符串"></component>,没有其他花里胡哨的东西。

举个最基础的例子吧,比如做一个简单的登录注册切换页,首先你得准备两个子组件,比如叫LoginForm.vue和RegisterForm.vue,然后在父组件里:

  1. 用import把两个组件引过来
  2. 用ref或者reactive定义一个当前展示组件的变量,比如const currentTab = ref('LoginForm')
  3. 最后把变量绑到is上,再写两个切换按钮就行

这里有个小细节新手容易忘:用import引入的组件,绑is的时候可以直接用变量(但要注意不能直接用字符串引未注册的组件,除非用全局注册或者<script setup>里的异步导入),如果是字符串的话,要么这个组件是全局注册过的,要么在当前父组件的<script setup>里用了defineAsyncComponent或者普通注册?不对,<script setup>里普通import的组件其实已经自动注册了,但绑字符串的时候只能用文件名去掉后缀的驼峰或者短横线写法,和Vue2不太一样——Vue2里<script setup>没普及前,字符串必须对应组件的name属性,Vue3里<script setup>默认没name的话,编译器会自动把文件名当name,所以可以直接绑。

为什么动态组件切换后输入框的内容全没了?怎么解决?

这应该是大家用动态组件遇到的第一个最头疼的问题,刚填完一半的密码,切到注册页再切回来,全空了,体验特别差,其实原因很简单:Vue3默认在切换组件的时候,会把旧组件直接卸载(unmount),再把新组件挂载(mount),组件里的所有状态都会重置。

解决方法也很明确:用Vue3自带的<KeepAlive>标签把<component>包起来就行。<KeepAlive>的作用就是“缓存”组件实例,不让它被卸载,状态自然就保留下来了。

但<KeepAlive>也不能随便包,比如有些场景不需要缓存所有组件,或者需要控制缓存的个数,这时候就要用它的三个属性:include、exclude、max。

比如只想缓存登录和注册页,不想缓存后面的密码重置页:可以给需要缓存的组件加个name属性(虽然<script setup>默认有,但最好显式加,避免短横线驼峰转换出问题,比如LoginForm显式写defineOptions({ name: 'LoginForm' })),然后include="LoginForm,RegisterForm"或者传数组include="['LoginForm', 'RegisterForm']"。

如果想限制缓存的最大组件数,比如只留最近切换过的3个:就加个max="3",这时候Vue会用LRU(最近最少使用)算法,自动把最久没用到的组件卸载掉。

还有个进阶用法:组件被缓存后,生命周期钩子会变,onMounted和onUnmounted只会在第一次挂载和最后一次卸载时触发,中间切换的时候会触发onActivated(组件被激活/展示)和onDeactivated(组件被失活/隐藏),比如登录页需要每次展示时获取验证码倒计时的剩余时间,就要把逻辑放在onActivated里,不能用onMounted。

动态组件怎么传参?父传子子传父都要搞清楚

很多人刚学会切换,就遇到传参的问题,其实动态组件传参和普通组件完全一样,别想复杂了。

父传子的话,直接在<component>标签上绑props就行,比如登录和注册页都需要一个agreementChecked的props:<component :is="currentTab" :agreement-checked="agreementChecked"></component>,子组件里用defineProps正常接收。

子传父的话,也是直接在<component>标签上绑自定义事件,比如登录成功后要通知父组件跳转首页:<component :is="currentTab" @login-success="handleLoginSuccess"></component>,子组件里用defineEmits正常触发就行。

这里有个小优化:如果父组件给所有子组件都传了相同的props或者绑了相同的事件,可以用v-bind="commonProps"和v-on="commonEvents"(或者v-on="commonEvents"在Vue3里可以简写为@="commonEvents"?不对,Vue3里v-on绑定对象是对的,但不能直接用,还是得老老实实写v-on或者一个个拆。

const commonProps = computed(() => ({
  agreementChecked: agreementChecked.value,
  theme: currentTheme.value
}))
const commonEvents = {
  'toggle-agreement': (val) => agreementChecked.value = val
}

然后<component :is="currentTab" v-bind="commonProps" v-on="commonEvents"></component>,这样代码会更整洁,尤其是有很多子组件的时候。

TypeScript项目里用动态组件,怎么避免“组件类型不兼容”的警告?

很多Vue3+TS的用户,刚写动态组件就会看到编辑器或者控制台飘红:“类型 'string | ComponentPublicInstanceConstructor<...>' 不能赋值给类型 'Component | null | undefined'”,或者直接渲染不出来组件。

这个问题的核心在于is的类型推断不对,Vue3给is属性定义的类型是Component | null | undefined,其中Component是DefineComponent或者异步组件的类型,但如果你用ref('LoginForm')定义变量,TypeScript会把它推断成string类型,自然就不兼容了。

解决方法有两个,分情况用:

子组件是固定的几个

这种情况最好用字面量联合类型+类型断言,或者直接用组件本身作为ref的初始值。

  • 直接用组件的话最简单:const currentTab = ref<DefineComponent>(LoginForm),切换的时候直接赋值currentTab.value = RegisterForm,不用管字符串的问题,TypeScript自动就推断对了。
  • 如果非要用字符串(比如需要从接口动态获取组件名?后面会讲接口获取的情况),就给ref加字面量联合类型的断言:const currentTab = ref<'LoginForm' | 'RegisterForm'>('LoginForm'),这样TypeScript就知道这个字符串只能是这两个组件的名字,和全局/本地注册的组件对应上了。

子组件是从接口动态获取的

这个情况稍微复杂一点,因为你不知道接口会返回什么组件名,这时候就要用异步组件的工厂函数+类型守卫,或者提前建立一个组件映射表。

  • 建立映射表的方法最常用也最稳妥:提前把所有可能用到的动态组件都import进来,然后建立一个键是组件名字符串、值是组件本身或者异步组件的对象,
    import { defineAsyncComponent, type DefineComponent } from 'vue'

// 普通组件映射 const staticComponents: Record<string, DefineComponent> = { LoginForm, RegisterForm } // 异步组件映射(大组件用这个,按需加载) const asyncComponents: Record<string, DefineComponent> = { PasswordResetForm: defineAsyncComponent(() => import('./PasswordResetForm.vue')), UserInfoForm: defineAsyncComponent(() => import('./UserInfoForm.vue')) } // 合并映射表 const componentMap: Record<string, DefineComponent> = { ...staticComponents, ...asyncComponents }

然后定义currentTab的时候,先从接口获取组件名,再从映射表里取:`const currentTab = ref<DefineComponent | null>(null)`,`const fetchComponent = async () => { const name = await getComponentName(); currentTab.value = componentMap[name] || null }`,这样不仅解决了类型问题,还能按需加载大组件,提升性能。
### 动态组件还有什么进阶用法?
除了上面讲的缓存、传参、TypeScript兼容,动态组件还有几个实用的进阶用法:
#### 1. 结合Teleport做弹窗是不一样的,比如确认弹窗、输入弹窗、选择弹窗,这时候可以用动态组件结合Teleport,只写一个Teleport容器,内容根据弹窗类型动态切换:
```html
<Teleport to="body">
  <div v-if="showModal" class="modal-container">
    <component :is="currentModalComponent" :modal-props="modalProps" @close-modal="showModal = false"></component>
  </div>
</Teleport>

做低代码渲染器的基础

低代码渲染器的核心就是“根据配置渲染组件”,动态组件就是这里的基石,比如配置里有个{ type: 'Input', props: { placeholder: '请输入姓名' } },你就可以建立一个表单组件的映射表,然后用<component :is="componentMap[config.type]" v-bind="config.props"></component>来渲染。

Vue3的动态组件不是什么高深的东西,只要把is、<KeepAlive>、生命周期钩子、TypeScript类型推断这几个点搞清楚,再避开卸载缓存的坑,它绝对能成为你开发中的得力助手。

版权声明

本文仅代表作者观点,不代表Code前端网立场。
本文系作者Code前端网发表,如需转载,请注明页面地址。

热门