在 Vue 3 中,動態組件是一個非常強大的特性,允許我們在運行時根據條件切換不同的組件。
基本用法
使用 <component> 標籤
<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. 帶屬性傳遞的動態組件
<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> 緩存組件狀態
<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. 異步組件加載
<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. 實際應用:可配置的卡片組件
<!-- 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>
使用這個動態卡片組件:
<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. 性能優化
// 使用 shallowRef 而不是 ref 來避免深層響應式
const currentComponent = shallowRef(MyComponent)
// 合理使用 keep-alive 的 include/exclude 屬性
<keep-alive :include="['ComponentA', 'ComponentB']">
<component :is="currentComponent" />
</keep-alive>
2. 類型安全(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. 錯誤處理
<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 中非常實用的特性,特別適合用於構建可複用、靈活的應用程序架構。通過合理使用這些模式,可以創建出既強大又易於維護的組件系統。