Vue 3 动态组件详解
在 Vue 3 中,动态组件是一个非常强大的特性,允许我们在运行时根据条件切换不同的组件。
基本用法
使用 <component> 标签
vue
<template>
<div>
<!-- 动态组件的核心 -->
<component :is="currentComponent"></component>
<!-- 切换按钮 -->
<button @click="switchComponent('Home')">首页</button>
<button @click="switchComponent('About')">关于</button>
<button @click="switchComponent('Contact')">联系</button>
</div>
</template>
<script setup>
import { ref, shallowRef } from 'vue'
import Home from './components/Home.vue'
import About from './components/About.vue'
import Contact from './components/Contact.vue'
// 使用 shallowRef 避免不必要的响应式转换
const currentComponent = shallowRef(Home)
const switchComponent = (componentName) => {
const components = {
Home,
About,
Contact
}
currentComponent.value = components[componentName]
}
</script>
高级用法示例
1. 带属性传递的动态组件
vue
<template>
<div>
<component
:is="currentView"
:title="componentTitle"
:data="componentData"
@custom-event="handleCustomEvent"
/>
<nav>
<button
v-for="view in views"
:key="view.name"
@click="changeView(view)"
:class="{ active: currentView === view.component }"
>
{{ view.label }}
</button>
</nav>
</div>
</template>
<script setup>
import { ref, shallowRef } from 'vue'
import UserProfile from './UserProfile.vue'
import UserSettings from './UserSettings.vue'
import UserDashboard from './UserDashboard.vue'
const currentView = shallowRef(UserProfile)
const componentTitle = ref('用户资料')
const componentData = ref({ userId: 123 })
const views = [
{ name: 'profile', label: '个人资料', component: UserProfile },
{ name: 'settings', label: '设置', component: UserSettings },
{ name: 'dashboard', label: '仪表板', component: UserDashboard }
]
const changeView = (view) => {
currentView.value = view.component
componentTitle.value = view.label
componentData.value = { ...componentData.value, viewType: view.name }
}
const handleCustomEvent = (payload) => {
console.log('接收到自定义事件:', payload)
}
</script>
<style scoped>
nav button.active {
background-color: #007bff;
color: white;
}
</style>
2. 使用 <keep-alive> 缓存组件状态
vue
<template>
<div>
<!-- 缓存动态组件的状态 -->
<keep-alive :include="cachedComponents">
<component :is="currentComponent" />
</keep-alive>
<div class="tabs">
<button
v-for="tab in tabs"
:key="tab.name"
@click="switchTab(tab.name)"
:class="{ active: activeTab === tab.name }"
>
{{ tab.label }}
</button>
</div>
</div>
</template>
<script setup>
import { ref, shallowRef } from 'vue'
import TabA from './TabA.vue'
import TabB from './TabB.vue'
import TabC from './TabC.vue'
const activeTab = ref('tab-a')
const currentComponent = shallowRef(TabA)
// 定义需要缓存的组件
const cachedComponents = ['TabA', 'TabB']
const tabs = [
{ name: 'tab-a', label: '标签页 A', component: TabA },
{ name: 'tab-b', label: '标签页 B', component: TabB },
{ name: 'tab-c', label: '标签页 C', component: TabC }
]
const switchTab = (tabName) => {
activeTab.value = tabName
const tab = tabs.find(t => t.name === tabName)
if (tab) {
currentComponent.value = tab.component
}
}
</script>
3. 异步组件加载
vue
<template>
<div>
<Suspense>
<template #default>
<component :is="asyncComponent" />
</template>
<template #fallback>
<div class="loading">加载中...</div>
</template>
</Suspense>
<button @click="loadComponent('HeavyChart')">加载图表</button>
<button @click="loadComponent('DataGrid')">加载数据表格</button>
</div>
</template>
<script setup>
import { shallowRef, defineAsyncComponent } from 'vue'
const asyncComponent = shallowRef(null)
const loadComponent = async (componentName) => {
try {
let component
switch (componentName) {
case 'HeavyChart':
component = defineAsyncComponent(() =>
import('./HeavyChart.vue')
)
break
case 'DataGrid':
component = defineAsyncComponent({
loader: () => import('./DataGrid.vue'),
loadingComponent: LoadingSpinner,
errorComponent: ErrorComponent,
delay: 200,
timeout: 3000
})
break
default:
return
}
asyncComponent.value = component
} catch (error) {
console.error('组件加载失败:', error)
}
}
// 加载指示器组件
const LoadingSpinner = {
template: '<div class="spinner">🌀 正在加载...</div>'
}
// 错误组件
const ErrorComponent = {
template: '<div class="error">❌ 组件加载失败</div>'
}
</script>
<style scoped>
.loading, .spinner, .error {
padding: 20px;
text-align: center;
}
.spinner {
color: #007bff;
}
.error {
color: #dc3545;
}
</style>
4. 实际应用:可配置的卡片组件
vue
<!-- DynamicCard.vue -->
<template>
<div class="dynamic-card">
<header class="card-header">
<h3>{{ config.title }}</h3>
<component
v-if="config.headerAction"
:is="config.headerAction.component"
v-bind="config.headerAction.props"
@action="handleHeaderAction"
/>
</header>
<main class="card-body">
<keep-alive>
<component
:is="config.content.component"
v-bind="config.content.props"
@update="handleContentUpdate"
/>
</keep-alive>
</main>
<footer v-if="config.footer" class="card-footer">
<component
:is="config.footer.component"
v-bind="config.footer.props"
@footer-action="handleFooterAction"
/>
</footer>
</div>
</template>
<script setup>
defineProps({
config: {
type: Object,
required: true,
validator(value) {
return value.title && value.content && value.content.component
}
}
})
const emit = defineEmits(['header-action', 'content-update', 'footer-action'])
const handleHeaderAction = (payload) => {
emit('header-action', payload)
}
const handleContentUpdate = (payload) => {
emit('content-update', payload)
}
const handleFooterAction = (payload) => {
emit('footer-action', payload)
}
</script>
<style scoped>
.dynamic-card {
border: 1px solid #ddd;
border-radius: 8px;
overflow: hidden;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 16px;
background-color: #f8f9fa;
border-bottom: 1px solid #ddd;
}
.card-body {
padding: 16px;
min-height: 200px;
}
.card-footer {
padding: 16px;
background-color: #f8f9fa;
border-top: 1px solid #ddd;
}
</style>
使用这个动态卡片组件:
vue
<template>
<DynamicCard :config="cardConfig" />
</template>
<script setup>
import { ref } from 'vue'
import DynamicCard from './DynamicCard.vue'
import UserInfo from './UserInfo.vue'
import ChartComponent from './ChartComponent.vue'
import ActionButtons from './ActionButtons.vue'
const cardConfig = ref({
title: '用户仪表板',
headerAction: {
component: 'button',
props: {
innerText: '刷新',
onClick: () => console.log('刷新数据')
}
},
content: {
component: ChartComponent,
props: {
data: [10, 20, 30, 40],
type: 'line'
}
},
footer: {
component: ActionButtons,
props: {
actions: ['导出', '分享', '打印']
}
}
})
</script>
最佳实践
1. 性能优化
javascript
// 使用 shallowRef 而不是 ref 来避免深层响应式
const currentComponent = shallowRef(MyComponent)
// 合理使用 keep-alive 的 include/exclude 属性
<keep-alive :include="['ComponentA', 'ComponentB']">
<component :is="currentComponent" />
</keep-alive>
2. 类型安全(TypeScript)
typescript
interface ComponentConfig {
name: string
component: Component
props?: Record<string, any>
events?: Record<string, Function>
}
const componentConfigs: ComponentConfig[] = [
{
name: 'home',
component: Home,
props: { title: '首页' }
}
]
3. 错误处理
vue
<script setup>
import { onErrorCaptured } from 'vue'
const hasError = ref(false)
onErrorCaptured((error, instance, info) => {
console.error('动态组件错误:', error, info)
hasError.value = true
return false
})
</script>
动态组件是 Vue 3 中非常实用的特性,特别适合用于构建可复用、灵活的应用程序架构。通过合理使用这些模式,可以创建出既强大又易于维护的组件系统。