动态组件【vue3实战详解】

学习不仅是为了应对挑战,更是为了创造更多机会给自己

目录

  • 动态组件-引言
    • [1. 动态组件是什么?](#1. 动态组件是什么?)
    • [2. 为什么要用动态组件(优点)](#2. 为什么要用动态组件(优点))
    • [3. 基本用法与语法](#3. 基本用法与语法)
      • [3.1 组件对象方式(推荐)](#3.1 组件对象方式(推荐))
      • [3.2 字符串方式(组件需已注册)](#3.2 字符串方式(组件需已注册))
    • [4. `:is` 可以传入哪些类型](#4. :is 可以传入哪些类型)
    • [5. 常见场景与示例](#5. 常见场景与示例)
      • [场景 A:Tab 切换](#场景 A:Tab 切换)
      • [场景 B:配置驱动的动态表单](#场景 B:配置驱动的动态表单)
      • [场景 C:弹窗内容切换](#场景 C:弹窗内容切换)
      • [场景 D:媒体展示(Text/Image/Video)](#场景 D:媒体展示(Text/Image/Video))
    • [6. keep-alive 与组件状态保留](#6. keep-alive 与组件状态保留)
    • [7. 异步组件(按需加载)](#7. 异步组件(按需加载))
    • [8. props / emits / attrs 在动态组件中的处理](#8. props / emits / attrs 在动态组件中的处理)
    • [9. 命名约定:驼峰 vs 短横线(kebab-case)](#9. 命名约定:驼峰 vs 短横线(kebab-case))
    • [10. 常见坑与调试建议](#10. 常见坑与调试建议)
    • [11. 性能与最佳实践](#11. 性能与最佳实践)
    • [12. TypeScript 相关提示](#12. TypeScript 相关提示)
    • [13. 小结与参考示例一览](#13. 小结与参考示例一览)

动态组件-引言

本文档为一份详细的动态组件介绍,面向使用 Vue3 + TypeScript 的前端工程师,详细讲解"动态组件"的概念、实现方式、常见场景、进阶技巧与注意事项,并包含多种可直接拷贝运行的示例代码。

1. 动态组件是什么?

动态组件就是通过变量在运行时渲染你需要渲染的那个组件:

typescript 复制代码
<component :is="currentComp" />

<component> 标签是 Vue 提供的内置占位组件。currentComp 可以是组件对象、组件名字符串,或异步组件(defineAsyncComponent)。

它可以把"选择渲染哪个组件"这件事从模板的 v-if / v-else 分支中抽离出来,使代码更简洁、可维护。具体可以根据具体情况去使用

2. 为什么要用动态组件(优点)

  • 简洁优雅 :避免大量 v-if / v-else-if
  • 配置驱动:后端或页面配置只需返回 type 字段,就能渲染不同组件。
  • 状态保留 :配合 <keep-alive> 可保留切换组件的内部状态(例如表单输入)。
  • 按需加载:配合异步组件可以减少首屏体积。
  • 复用 UI 容器:一个弹窗或面板可以渲染多种内容,提升复用度。

3. 基本用法与语法

3.1 组件对象方式(推荐)

typescript 复制代码
<script setup lang="ts">
import { ref, computed } from 'vue'
import TextComp from './TextComp.vue' // 文字组件
import ImageComp from './ImageComp.vue' // 图片组件
import VideoComp from './VideoComp.vue'

const currentType = ref('text')
const compMap: Record<string, any> = {
  text: TextComp,
  image: ImageComp,
  video:VideoComp
}
const currentComp = computed(() => compMap[currentType.value])
</script>

<template>
  <button @click="currentType = 'text'">渲染Text组件</button>
  <button @click="currentType = 'image'">渲染Image组件</button>
  <button @click="currentType = 'video'">渲染Video组件</button>

  <component :is="currentComp" />
</template>

也可以通过后端给到的直接用v-for依次渲染组件哦~

3.2 字符串方式(组件需已注册)

typescript 复制代码
<component :is="'text-comp'" />

注意:如果你写 <component :is="text-comp">(没有引号也不是变量),那会被当作 JS 表达式并抛错。:is 要么是字符串(需存在已注册组件),要么是组件对象变量。

4. :is 可以传入哪些类型

  • 组件对象(本地 import 导入的组件)
  • 字符串(组件名,必须全局注册或已在 components 中注册)
  • 异步组件(defineAsyncComponent(() => import('./A.vue'))
  • nullundefined(相当于什么也不渲染)

示例异步组件:

typescript 复制代码
import { defineAsyncComponent } from 'vue'
const AsyncPage = defineAsyncComponent(() => import('./PageA.vue'))

const compMap = {
  a: AsyncPage,
}

5. 常见场景与示例

场景 A:Tab 切换

typescript 复制代码
<script setup lang="ts">
import { ref, computed } from 'vue'
import TabA from './TabA.vue'
import TabB from './TabB.vue'

const active = ref('a')
const map = { a: TabA, b: TabB }
const comp = computed(() => map[active.value])
</script>
<template>
  <button @click="active='a'">A</button>
  <button @click="active='b'">B</button>
  <component :is="comp" />
</template>

场景 B:配置驱动的动态表单

服务器返回字段:

json 复制代码
[{ "type":"input", "label":"姓名" }, { "type":"select", "label":"性别" }]

组件实现(伪代码):

typescript 复制代码
<script setup lang="ts">
import InputField from './InputField.vue'
import SelectField from './SelectField.vue'

const widgetMap = { input: InputField, select: SelectField }
const fields = ref([{ type:'input', label:'姓名' }, { type:'select', label:'性别' }])
</script>
<template>
  <div v-for="(f,i) in fields" :key="i">
    <component :is="widgetMap[f.type]" v-bind="f" />
  </div>
</template>

这种方式非常适合后台配置化表单、页面渲染配置等。

场景 C:弹窗内容切换

一个 Modal 里展示不同子组件:

typescript 复制代码
<Modal>
  <component :is="dialogComp" v-bind="dialogProps" />
</Modal>

父组件根据业务场景把 dialogCompdialogProps 传给 Modal (这里写的v-bind是单向绑定->从父组件向子组件传递 props)。

场景 D:媒体展示(Text/Image/Video)

typescript 复制代码
<script setup lang="ts">
import { ref, computed } from 'vue'
import TextComp from './TextComp.vue'
import ImageComp from './ImageComp.vue'
import VideoComp from './VideoComp.vue'

const currentType = ref('text')
const map = { text: TextComp, image: ImageComp, video: VideoComp }
const currentComp = computed(() => map[currentType.value])
</script>
<template>
  <div>
    <button @click="currentType='text'">文本</button>
    <button @click="currentType='image'">图片</button>
    <button @click="currentType='video'">视频</button>

    <component :is="currentComp" :some-prop="123" />
  </div>
</template>

6. keep-alive 与组件状态保留

包裹在 <keep-alive> 内的动态组件在切换时不会被销毁(而是缓存起来),再次切回时恢复之前的状态。

typescript 复制代码
<keep-alive>
  <component :is="currentComp" />
</keep-alive>

常用 props:

  • include / exclude:通过组件名字符串匹配决定缓存哪些组件。
  • max:最大缓存数量。

注意:include / exclude 匹配的是组件的 name(组件 name 属性或文件名作为自动推断),如果你用匿名组件对象注册,可能无法匹配。

合理使用 keep-alive 可以提升性能,但要避免无节制地缓存所有组件,以防内存占用过高。

7. 异步组件(按需加载)

点击跳转 异步组件讲解:defineAsyncComponent【Vue3】

减少首屏体积常用方案:

typescript 复制代码
import { defineAsyncComponent } from 'vue'
const AsyncComp = defineAsyncComponent(() => import('./Heavy.vue'))

然后:

typescript 复制代码
<component :is="AsyncComp" />

还可以配合加载中/错误组件:

typescript 复制代码
const AsyncComp = defineAsyncComponent({
  loader: () => import('./A.vue'),
  loadingComponent: LoadingComp,
  errorComponent: ErrorComp,
  delay: 200,
  timeout: 3000
})

8. props / emits / $attrs 在动态组件中的处理

  • 通过 v-bind 把 props 传给动态组件:<component :is="comp" v-bind="someProps" />
  • $attrs 会包含父组件传入但子组件未声明的属性(包括未声明事件),可以用于透传到真实 DOM 或内部子组件。
  • emits 声明的事件不会出现在 $attrs 中(它们应由 emit 处理)。

透传示例

typescript 复制代码
<!-- Wrapper.vue -->
<template>
  <div>
    <component :is="comp" v-bind="$attrs" v-on="$attrs" />
  </div>
</template>
  • $attrs 是 Vue 提供的一个对象,它包含了 父组件传递给当前组件的所有 prop 以外的属性和事件监听器(包括 class、style、@click 等)。
  • v-on="$attrs" 用于绑定所有的事件监听器,它会将 $attrs 对象中的所有事件监听器(如 @click、@input 等)绑定到当前组件的根元素或者动态组件上。

注意 :事件的透传需要特别小心,建议显式声明 emits 或者手动转发事件以免意外行为。

9. 命名约定:驼峰 vs 短横线(kebab-case)

  • JS/TS 中 导入或注册组件通常使用 驼峰 (PascalCase / camelCase),例如 TextComp
  • 模板中 书写标签通常使用 短横线(kebab-case) ,例如 <text-comp />

关键点

  • :is 中,传组件对象时使用变量名(驼峰):is="TextComp"
  • 传字符串时使用短横线字符串:is="'text-comp'",且组件必须已注册。
  • 错误写法:is="text-comp"(没有引号且不是变量)会被当作 JS 表达式导致报错。

10. 常见坑与调试建议

  • :is="text-comp"(漏引号)会报未定义变量错误。
  • 使用字符串方式时请确保组件已注册(局部/全局);否则会渲染空节点(vue3会自动引入并注册组件,无需显式注册)。
  • keep-alive 匹配 include / exclude 依赖组件 name,匿名组件可能无法匹配。
  • 传 props 时注意类型(TypeScript),动态组件需要正确传递 key 以控制销毁/重建行为。
  • 如果组件切换后仍然看到旧组件内容,检查是否用了 keep-alive 或缓存逻辑。

Tipssssssss :在开发时把组件 name 明确写上,便于 keep-alive 匹配与 Vue DevTools 识别。

11. 性能与最佳实践

  • 使用异步组件分割代码,减少首屏体积。
  • 对于频繁切换且不需要保留状态的组件,避免滥用 keep-alive,否则会占用内存。
  • 对大表单或组件使用 key 控制强制重建:
vue 复制代码
<component :is="comp" :key="someUniqueKey" />
  • 使用映射表(compMap)来管理类型到组件的映射,便于扩展与维护。
  • 尽量避免在模板中写复杂逻辑,使用 computed/methods 在 setup 中处理。

12. TypeScript 相关提示

  • compMap 指定类型:
ts 复制代码
import type { Component } from 'vue'
const compMap: Record<string, Component> = { text: TextComp }
  • 若组件有 props,确保在模板传入的 props 与组件声明一致,或使用 as 断言来避免泛型冲突。
  • 对异步组件使用 defineAsyncComponent 并给返回类型 Component

13. 小结与参考示例一览

要点回顾:

  • 动态组件通过 <component :is="..."> 在运行时切换渲染目标。
  • :is 支持组件对象与字符串(字符串需已注册);异步组件也受支持。
  • 使用 compMap + computed 的模式是非常推荐的实践。
  • keep-alive 能保留状态但要合理使用。
  • 注意 kebab-case 与 camelCase 的写法差异,不要把组件名写成未定义变量。

参考代码(完整可运行示例)

src/components/ 下创建:

  • TextComp.vue
typescript 复制代码
<template>
  <div>文本组件:{{ msg }}</div>
</template>
<script setup lang="ts">
import { defineProps } from 'vue'
const props = defineProps<{ msg?: string }>()
</script>
  • ImageComp.vue
typescript 复制代码
<template>
  <div>图片组件:<img :src="src" alt="img" style="max-width:200px" /></div>
</template>
<script setup lang="ts">
const props = defineProps<{ src: string }>()
</script>
  • VideoComp.vue
typescript 复制代码
<template>
  <div>视频组件:<video :src="src" controls style="max-width:300px"/></div>
</template>
<script setup lang="ts">
const props = defineProps<{ src: string }>()
</script>
  • DynamicShow.vue(父组件):
typescript 复制代码
<script setup lang="ts">
import { ref, computed } from 'vue'
import TextComp from './TextComp.vue'
import ImageComp from './ImageComp.vue'
import VideoComp from './VideoComp.vue'

const type = ref<'text'|'image'|'video'>('text')
const map: Record<string, any> = { text: TextComp, image: ImageComp, video: VideoComp }
const comp = computed(() => map[type.value])
</script>
<template>
  <div>
    <button @click="type='text'">文本</button>
    <button @click="type='image'">图片</button>
    <button @click="type='video'">视频</button>

    <keep-alive>
      <component :is="comp" :src="type==='image' ? 'https://picsum.photos/200' : 'video.mp4'" msg="hello" />
    </keep-alive>
  </div>
</template>
相关推荐
Duck不必1 小时前
紧急插播:CVSS 10.0 满分漏洞!你的 Next.js 项目可能正在裸奔
前端·next.js
+VX:Fegn08951 小时前
计算机毕业设计|基于springboot + vue在线考试管理系统(源码+数据库+文档)
数据库·vue.js·spring boot·后端·课程设计
用户413079810611 小时前
终于不漏了-Android开发内存泄漏详解
前端
孟祥_成都1 小时前
nest.js / hono.js 一起学!hono的设计思想!
前端·node.js
努力glow .1 小时前
彻底解决VMware下ROS2中gazebo启动失败的问题
前端·chrome
阿笑带你学前端1 小时前
开源记账 App 一个月迭代:从 v1.11 到 v2.2,暗黑模式、标签系统、预算管理全面升级
前端
AAA阿giao1 小时前
浏览器底层探秘:Chrome的奇妙世界
前端·chrome·gpu·多进程·单进程·v8引擎·浏览器底层
王兆龙1681 小时前
Vue3组件传值
前端·javascript·vue.js
随风一样自由2 小时前
React中实现iframe嵌套登录页面:跨域与状态同步解决方案详解
前端·react.js·前端框架·跨域