Vue 3 动态组件详解

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 中非常实用的特性,特别适合用于构建可复用、灵活的应用程序架构。通过合理使用这些模式,可以创建出既强大又易于维护的组件系统。

相关推荐
叫我詹躲躲8 小时前
基于 Three.js 的 3D 地图可视化:核心原理与实现步骤
前端·three.js
TimelessHaze8 小时前
算法复杂度分析与优化:从理论到实战
前端·javascript·算法
San308 小时前
现代前端工程化实战:从 Vite 到 Vue Router 的构建之旅
vue.js·vite·vue-router
_请输入用户名8 小时前
打开Vue3的黑匣子:工程结构背后的设计哲学
vue.js·前端框架
旧梦星轨8 小时前
掌握 Vite 环境配置:从 .env 文件到运行模式的完整实践
前端·前端框架·node.js·vue·react
PieroPC8 小时前
NiceGui 3.4.0 的 ui.pagination 分页实现 例子
前端·后端
晚霞的不甘8 小时前
实战前瞻:构建高可用、强实时的 Flutter + OpenHarmony 智慧医疗健康平台
前端·javascript·flutter
精神病不行计算机不上班8 小时前
[Java Web]Java Servlet基础
java·前端·servlet·html·mvc·web·session
玉木成琳8 小时前
Taro + React + @nutui/nutui-react-taro 时间选择器重写
前端·react.js·taro