博客 / 詳情

返回

Vue 3 動態組件詳解

在 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 中非常實用的特性,特別適合用於構建可複用、靈活的應用程序架構。通過合理使用這些模式,可以創建出既強大又易於維護的組件系統。

user avatar
0 位用戶收藏了這個故事!

發佈 評論

Some HTML is okay.