Appearance
动态组件与异步组件
动态组件
Vue.js提供了component元素的特殊属性,允许我们在同一个挂载点动态切换不同的组件。
基础用法
使用is属性来动态切换组件:
vue
<template>
<div>
<button @click="currentTab = 'Home'">Home</button>
<button @click="currentTab = 'About'">About</button>
<button @click="currentTab = 'Contact'">Contact</button>
<component :is="currentTabComponent" />
</div>
</template>
<script>
import Home from './components/Home.vue'
import About from './components/About.vue'
import Contact from './components/Contact.vue'
export default {
components: {
Home,
About,
Contact
},
data() {
return {
currentTab: 'Home'
}
},
computed: {
currentTabComponent() {
return this.currentTab
}
}
}
</script>
</template>
使用组件选项对象
除了使用注册的组件名,还可以直接使用组件选项对象:
vue
<template>
<div>
<button @click="switchComponent('red')">Red Component</button>
<button @click="switchComponent('blue')">Blue Component</button>
<component :is="currentComponent" />
</div>
</template>
<script>
const RedComponent = {
template: '<div style="color: red;">Red Component</div>'
}
const BlueComponent = {
template: '<div style="color: blue;">Blue Component</div>'
}
export default {
data() {
return {
currentComponent: RedComponent
}
},
methods: {
switchComponent(color) {
this.currentComponent = color === 'red' ? RedComponent : BlueComponent
}
}
}
</script>
</template>
保持组件状态
默认情况下,Vue会在切换动态组件时销毁和重建组件实例。如果需要保持组件状态,可以使用keep-alive元素:
vue
<template>
<div>
<button @click="currentView = 'Home'">Home</button>
<button @click="currentView = 'About'">About</button>
<!-- 使用 keep-alive 保持组件状态 -->
<keep-alive>
<component :is="currentView" />
</keep-alive>
</div>
</template>
<script>
import Home from './components/Home.vue'
import About from './components/About.vue'
export default {
components: {
Home,
About
},
data() {
return {
currentView: 'Home'
}
}
}
</script>
</template>
组合式API中的动态组件
在组合式API中,动态组件的使用方式类似:
vue
<script setup>
import { ref } from 'vue'
import Home from './components/Home.vue'
import About from './components/About.vue'
import Contact from './components/Contact.vue'
const currentView = ref('Home')
// 组件选项对象
const components = {
Home,
About,
Contact
}
function changeView(view) {
currentView.value = view
}
</script>
<template>
<div>
<nav>
<button @click="changeView('Home')">Home</button>
<button @click="changeView('About')">About</button>
<button @click="changeView('Contact')">Contact</button>
</nav>
<keep-alive>
<component :is="currentView" />
</keep-alive>
</div>
</template>
异步组件
Vue.js支持异步组件,允许将组件定义为一个工厂函数,该函数异步解析组件定义。这使得我们可以实现组件的懒加载,减少初始包大小。
基础异步组件
javascript
// AsyncComponent.js
import { defineAsyncComponent } from 'vue'
const AsyncComponent = defineAsyncComponent(() => {
return import('./components/ExpensiveComponent.vue')
})
export default AsyncComponent
vue
<template>
<div>
<h1>Page with Async Component</h1>
<AsyncComponent v-if="showComponent" />
<button @click="showComponent = true">Load Component</button>
</div>
</template>
<script>
import { defineAsyncComponent } from 'vue'
export default {
components: {
AsyncComponent: defineAsyncComponent(() => import('./components/ExpensiveComponent.vue'))
},
data() {
return {
showComponent: false
}
}
}
</script>
</template>
带加载状态的异步组件
可以为异步组件提供加载状态、错误状态等选项:
javascript
import { defineAsyncComponent } from 'vue'
const AsyncComponent = defineAsyncComponent({
// 加载组件的函数
loader: () => import('./components/ExpensiveComponent.vue'),
// 加载状态下的组件
loadingComponent: LoadingComponent,
// 错误状态下的组件
errorComponent: ErrorComponent,
// 延迟显示加载组件的时间 (默认 200ms)
delay: 200,
// 超时时间 (默认: Infinity)
timeout: 3000,
// 加载成功后的回调
onSuccess: () => {
console.log('Component loaded successfully')
},
// 加载失败后的回调
onError: (error, retry, fail, attempts) => {
console.log('Component loading failed', error)
// 可以选择重试
if (attempts <= 3) {
retry()
} else {
fail()
}
}
})
组合式API中的异步组件
vue
<script setup>
import { defineAsyncComponent, ref } from 'vue'
// 定义异步组件
const AsyncChart = defineAsyncComponent({
loader: () => import('./components/ChartComponent.vue'),
loadingComponent: {
template: '<div>Loading chart...</div>'
},
errorComponent: {
template: '<div>Failed to load chart</div>'
},
delay: 200,
timeout: 5000
})
const AsyncDataTable = defineAsyncComponent(() =>
import('./components/DataTable.vue')
)
const showChart = ref(false)
const showTable = ref(false)
</script>
<template>
<div>
<h1>Dashboard</h1>
<button @click="showChart = true" :disabled="showChart">
Load Chart
</button>
<button @click="showTable = true" :disabled="showTable">
Load Data Table
</button>
<div v-if="showChart">
<h2>Chart</h2>
<Suspense>
<template #default>
<AsyncChart />
</template>
<template #fallback>
<div>Loading chart component...</div>
</template>
</Suspense>
</div>
<div v-if="showTable">
<h2>Data Table</h2>
<Suspense>
<template #default>
<AsyncDataTable />
</template>
<template #fallback>
<div>Loading data table...</div>
</template>
</Suspense>
</div>
</div>
</template>
Suspense 组件
Suspense是Vue 3引入的内置组件,用于在组件树中协调对异步依赖的处理。
基础Suspense用法
vue
<template>
<div>
<h1>Async Content</h1>
<Suspense>
<template #default>
<AsyncComponent />
</template>
<template #fallback>
<div>Loading...</div>
</template>
</Suspense>
</div>
</template>
<script>
import { defineAsyncComponent } from 'vue'
const AsyncComponent = defineAsyncComponent(() => import('./components/AsyncComponent.vue'))
export default {
components: {
AsyncComponent
}
}
</script>
</template>
嵌套Suspense
可以使用嵌套的Suspense组件来处理不同层级的异步依赖:
vue
<template>
<div>
<h1>Main Content</h1>
<Suspense>
<template #default>
<MainContent />
</template>
<template #fallback>
<div>Loading main content...</div>
</template>
</Suspense>
</div>
</template>
<!-- MainContent.vue -->
<template>
<div>
<h2>Section 1</h2>
<Suspense>
<template #default>
<AsyncSection1 />
</template>
<template #fallback>
<div>Loading section 1...</div>
</template>
</Suspense>
<h2>Section 2</h2>
<Suspense>
<template #default>
<AsyncSection2 />
</template>
<template #fallback>
<div>Loading section 2...</div>
</template>
</Suspense>
</div>
</template>
高级动态组件用法
带缓存的动态组件
结合keep-alive和动态组件来缓存组件状态:
vue
<script setup>
import { ref, computed } from 'vue'
import Home from './components/Home.vue'
import About from './components/About.vue'
import Profile from './components/Profile.vue'
const currentView = ref('Home')
const cachedViews = ref(['Home']) // 需要缓存的视图
const components = {
Home,
About,
Profile
}
function activateView(view) {
currentView.value = view
if (!cachedViews.value.includes(view)) {
cachedViews.value.push(view)
}
}
function removeCachedView(view) {
const index = cachedViews.value.indexOf(view)
if (index > -1) {
cachedViews.value.splice(index, 1)
}
}
</script>
<template>
<div>
<nav>
<button @click="activateView('Home')">Home</button>
<button @click="activateView('About')">About</button>
<button @click="activateView('Profile')">Profile</button>
</nav>
<div class="view-controls">
<button
v-for="view in cachedViews"
:key="view"
@click="removeCachedView(view)"
>
Remove {{ view }} from cache
</button>
</div>
<keep-alive :include="cachedViews">
<component :is="currentView" />
</keep-alive>
</div>
</template>
条件动态组件
根据条件动态渲染不同的组件:
vue
<script setup>
import { computed } from 'vue'
import AdminDashboard from './components/AdminDashboard.vue'
import UserDashboard from './components/UserDashboard.vue'
import GuestDashboard from './components/GuestDashboard.vue'
const props = defineProps({
userRole: String,
isAuthenticated: Boolean
})
const currentDashboard = computed(() => {
if (!props.isAuthenticated) {
return 'GuestDashboard'
} else if (props.userRole === 'admin') {
return 'AdminDashboard'
} else {
return 'UserDashboard'
}
})
const components = {
AdminDashboard,
UserDashboard,
GuestDashboard
}
</script>
<template>
<div>
<component :is="currentDashboard" />
</div>
</template>
异步组件最佳实践
1. 路由级别的代码分割
在Vue Router中使用异步组件实现路由级别的代码分割:
javascript
import { createRouter, createWebHistory } from 'vue-router'
const routes = [
{
path: '/',
name: 'Home',
component: () => import('../views/Home.vue')
},
{
path: '/about',
name: 'About',
component: () => import('../views/About.vue')
},
{
path: '/profile',
name: 'Profile',
component: () => import('../views/Profile.vue')
}
]
const router = createRouter({
history: createWebHistory(),
routes
})
export default router
2. 条件加载异步组件
vue
<script setup>
import { defineAsyncComponent, ref, computed } from 'vue'
const showAdvancedFeature = ref(false)
// 仅在需要时才加载复杂组件
const AdvancedChart = computed(() => {
if (showAdvancedFeature.value) {
return defineAsyncComponent(() => import('./components/AdvancedChart.vue'))
}
return null
})
function toggleAdvancedFeature() {
showAdvancedFeature.value = true
}
</script>
<template>
<div>
<button
@click="toggleAdvancedFeature"
:disabled="showAdvancedFeature"
>
{{ showAdvancedFeature ? 'Feature Loaded' : 'Load Advanced Feature' }}
</button>
<div v-if="showAdvancedFeature && AdvancedChart">
<Suspense>
<template #default>
<component :is="AdvancedChart" />
</template>
<template #fallback>
<div>Loading advanced chart...</div>
</template>
</Suspense>
</div>
</div>
</template>
3. 错误处理和重试机制
javascript
import { defineAsyncComponent } from 'vue'
const AsyncComponentWithErrorHandling = defineAsyncComponent({
loader: () => import('./components/Component.vue'),
loadingComponent: {
template: '<div>Loading...</div>'
},
errorComponent: {
props: ['retry'],
template: `
<div>
<p>Failed to load component</p>
<button @click="retry">Retry</button>
</div>
`
},
delay: 200,
timeout: 10000,
onError: (error, retry, fail, attempts) => {
if (error.message.includes('Network Error') && attempts <= 3) {
// 网络错误时重试
setTimeout(() => retry(), 1000)
} else {
// 其他错误直接失败
fail()
}
}
})
实际应用示例
选项卡组件
vue
<script setup>
import { ref, defineAsyncComponent } from 'vue'
const tabs = ref([
{ id: 'dashboard', title: 'Dashboard', component: 'Dashboard' },
{ id: 'analytics', title: 'Analytics', component: 'Analytics' },
{ id: 'settings', title: 'Settings', component: 'Settings' }
])
const currentTab = ref('dashboard')
// 定义异步组件
const Dashboard = defineAsyncComponent(() => import('./tabs/Dashboard.vue'))
const Analytics = defineAsyncComponent(() => import('./tabs/Analytics.vue'))
const Settings = defineAsyncComponent(() => import('./tabs/Settings.vue'))
const components = {
Dashboard,
Analytics,
Settings
}
</script>
<template>
<div class="tab-container">
<div class="tab-nav">
<button
v-for="tab in tabs"
:key="tab.id"
@click="currentTab = tab.id"
:class="{ active: currentTab === tab.id }"
>
{{ tab.title }}
</button>
</div>
<div class="tab-content">
<keep-alive>
<component :is="components[tab.component]" v-if="currentTab === tab.id" />
</keep-alive>
</div>
</div>
</template>
<style>
.tab-container {
border: 1px solid #ddd;
border-radius: 4px;
}
.tab-nav {
display: flex;
border-bottom: 1px solid #ddd;
}
.tab-nav button {
padding: 12px 20px;
border: none;
background: none;
cursor: pointer;
}
.tab-nav button.active {
border-bottom: 3px solid #42b983;
color: #42b983;
}
.tab-content {
padding: 20px;
}
</style>
动态表单生成器
vue
<script setup>
import { ref, defineAsyncComponent } from 'vue'
const formConfig = ref([
{ type: 'text', component: 'InputField', props: { label: 'Name', required: true } },
{ type: 'email', component: 'InputField', props: { label: 'Email', required: true } },
{ type: 'select', component: 'SelectField', props: { label: 'Role', options: ['Admin', 'User', 'Guest'] } },
{ type: 'checkbox', component: 'CheckboxField', props: { label: 'Agree to terms' } }
])
// 异步加载表单组件
const InputField = defineAsyncComponent(() => import('./form-fields/InputField.vue'))
const SelectField = defineAsyncComponent(() => import('./form-fields/SelectField.vue'))
const CheckboxField = defineAsyncComponent(() => import('./form-fields/CheckboxField.vue'))
const components = {
InputField,
SelectField,
CheckboxField
}
</script>
<template>
<form class="dynamic-form">
<component
v-for="(field, index) in formConfig"
:key="index"
:is="components[field.component]"
v-bind="field.props"
/>
<button type="submit">Submit</button>
</form>
</template>
动态组件和异步组件是Vue.js中非常重要的功能,它们提供了灵活的组件切换和性能优化手段。通过合理使用这些功能,可以构建出高性能、可维护的Vue应用。