学习不仅是为了应对挑战,更是为了创造更多机会给自己
目录
- 动态组件-引言
-
- [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'))) null或undefined(相当于什么也不渲染)
示例异步组件:
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>
父组件根据业务场景把 dialogComp、dialogProps 传给 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>