一、从 Tab 切换说起
假设你接到个需求:做一个后台管理页面,左侧菜单点击后,右侧内容区跟着变。菜单有好几项,每个菜单对应一个组件。你可能会想用 v-if / v-else 一个一个判断:
vue
<template>
<div>
<button @click="current = 'home'">首页</button>
<button @click="current = 'users'">用户管理</button>
<button @click="current = 'settings'">系统设置</button>
<Home v-if="current === 'home'" />
<Users v-else-if="current === 'users'" />
<Settings v-else-if="current === 'settings'" />
</div>
</template>
如果只有三两个还好,后面菜单加到十几个,就要写一长串 v-if,又丑又不好维护。这时候就该动态组件出场了。
二、动态组件:一个标签搞定组件切换
Vue 内置了一个 <component> 标签,专门用来做动态组件。它有一个 is 属性,指向哪个组件就渲染哪个组件。
2.1 基础用法
vue
<template>
<div>
<!-- 三个按钮,点击改变 currentComponent 的值 -->
<button @click="currentComponent = 'Home'">首页</button>
<button @click="currentComponent = 'Users'">用户</button>
<button @click="currentComponent = 'Settings'">设置</button>
<!--
动态组件标签:<component :is="组件名" />
当 currentComponent 是 'Home' 时,渲染 Home 组件
当 currentComponent 是 'Users' 时,渲染 Users 组件
-->
<component :is="currentComponent" />
</div>
</template>
<script setup>
import { ref } from 'vue'
// 引入需要用到的组件
import Home from './Home.vue'
import Users from './Users.vue'
import Settings from './Settings.vue'
// 当前显示的组件名
const currentComponent = ref('Home')
</script>
发生了什么:
-
<component :is="xxx">里的xxx可以是组件的名字(字符串) ,也可以是组件对象本身。 -
上面我们传的是字符串
'Home',Vue 会自动去找同名的组件。 -
但更推荐传组件对象本身,因为这样不用关心名字映射。
2.2 传组件对象的方式(更稳妥)
vue
<template>
<div>
<button @click="current = homeComp">首页</button>
<button @click="current = usersComp">用户</button>
<button @click="current = settingsComp">设置</button>
<!-- 直接传组件对象,current 是哪个组件就渲染哪个 -->
<component :is="current" />
</div>
</template>
<script setup>
import { ref } from 'vue'
import Home from './Home.vue'
import Users from './Users.vue'
import Settings from './Settings.vue'
// 把组件对象存成变量
const homeComp = Home
const usersComp = Users
const settingsComp = Settings
// current 直接存组件对象
const current = ref(Home)
</script>
好处: 不管组件名字怎么改,只要对象不变就行,而且 TypeScript 类型推导也更友好。
三、配合 KeepAlive 缓存组件状态
动态组件切换时,离开的组件默认会被销毁 ,再切回来时重新创建,之前输入的内容、滚动位置全没了。如果想要保留状态,就用之前学过的 <KeepAlive> 包一层。
vue
<template>
<div>
<button @click="current = homeComp">首页</button>
<button @click="current = usersComp">用户</button>
<button @click="current = settingsComp">设置</button>
<!-- KeepAlive 包裹动态组件,离开时组件不销毁,状态还在 -->
<KeepAlive>
<component :is="current" />
</KeepAlive>
</div>
</template>
<script setup>
import { ref } from 'vue'
import Home from './Home.vue'
import Users from './Users.vue'
import Settings from './Settings.vue'
const homeComp = Home
const usersComp = Users
const settingsComp = Settings
const current = ref(Home)
</script>
效果: 你在"用户"页面填了一半的表单,切到"首页"看一眼,再切回"用户",表单内容还在。这就是 KeepAlive 的缓存效果。
四、异步组件:按需加载,提升首屏速度
前面我们都是用 import 直接引入组件,这叫静态导入。项目一大,首页可能会把所有组件代码都下载下来,首屏加载很慢。
异步组件 就是:等用到的时候才去加载对应的代码。比如用户点击"系统设置"时才去下载 Settings 组件的代码,平时不加载。
4.1 用 defineAsyncComponent 定义异步组件
vue
<script setup>
import { defineAsyncComponent } from 'vue'
// 定义一个异步组件,参数是一个函数,返回 import()
const AsyncHome = defineAsyncComponent(() => import('./Home.vue'))
const AsyncUsers = defineAsyncComponent(() => import('./Users.vue'))
const AsyncSettings = defineAsyncComponent(() => import('./Settings.vue'))
</script>
defineAsyncComponent 的作用: 接收一个返回 Promise 的函数(这里是 import()),组件真正需要渲染时才执行这个函数下载代码。
4.2 异步组件 + 动态组件 + KeepAlive 组合
vue
<template>
<div>
<button @click="current = asyncHome">首页</button>
<button @click="current = asyncUsers">用户</button>
<button @click="current = asyncSettings">设置</button>
<KeepAlive>
<component :is="current" />
</KeepAlive>
</div>
</template>
<script setup>
import { ref, defineAsyncComponent } from 'vue'
// 定义异步组件,用到时才加载
const asyncHome = defineAsyncComponent(() => import('./Home.vue'))
const asyncUsers = defineAsyncComponent(() => import('./Users.vue'))
const asyncSettings = defineAsyncComponent(() => import('./Settings.vue'))
const current = ref(asyncHome)
</script>
效果:
-
首屏只下载 Home 的代码,Users 和 Settings 的代码不下载。
-
第一次点击"用户"时,浏览器才去下载 Users.vue,这期间可以显示一个 loading 状态(下面会讲怎么加)。
-
第二次点"用户"时,因为已经有了缓存,瞬间出来。
4.3 异步组件加载中/加载失败的处理
defineAsyncComponent 支持传入一个配置对象,定制 loading 和 error 状态。
javascript
const asyncUsers = defineAsyncComponent({
// 加载函数,返回 import()
loader: () => import('./Users.vue'),
// 加载中显示的组件(在 Users.vue 还没下载完时显示)
loadingComponent: LoadingSpinner, // 你需要自己定义这个 loading 组件
// 加载中组件的延迟显示时间(毫秒)
// 如果 200ms 内加载完成,就不显示 loading 了,避免闪一下
delay: 200,
// 加载失败时显示的组件
errorComponent: ErrorDisplay, // 你需要自己定义这个 error 组件
// 加载失败后,多久再尝试重新加载(毫秒)
timeout: 3000
})
自定义 Loading 组件示例:
vue
<!-- LoadingSpinner.vue -->
<template>
<div class="loading">
<span>加载中,请稍候...</span>
</div>
</template>
自定义 Error 组件示例:
vue
<!-- ErrorDisplay.vue -->
<template>
<div class="error">
<p>组件加载失败</p>
<button @click="$emit('retry')">点击重试</button>
</div>
</template>
然后在配置里:
javascript
const asyncSettings = defineAsyncComponent({
loader: () => import('./Settings.vue'),
loadingComponent: LoadingSpinner,
delay: 200,
errorComponent: ErrorDisplay,
timeout: 3000,
// 重试机制:errorComponent 内部 emit('retry') 时,Vue 会自动调用 loader 重新加载
})
五、实战案例:完整的异步 Tab 切换面板
我们把上面学的东西综合起来,做一个完整的功能:底部有三个 Tab 图标,分别是"首页""发现""我的",点击切换组件,每个组件异步加载,切换时保持状态(KeepAlive),并且每个组件有 loading 和 error 处理。
5.1 项目结构
text
src/
├── components/
│ ├── TabBar.vue # 底部导航栏
│ ├── LoadingSpinner.vue # 通用 loading 组件
│ ├── ErrorDisplay.vue # 通用 error 组件
│ └── tabs/
│ ├── HomeTab.vue # 首页组件
│ ├── DiscoverTab.vue # 发现组件
│ └── ProfileTab.vue # 我的组件
└── App.vue
5.2 通用 Loading 组件
vue
<!-- LoadingSpinner.vue -->
<template>
<div class="spinner">
<span>🌀 加载中...</span>
</div>
</template>
<style scoped>
.spinner {
text-align: center;
padding: 40px;
color: #999;
}
</style>
5.3 通用 Error 组件
vue
<!-- ErrorDisplay.vue -->
<template>
<div class="error">
<p>😵 加载失败</p>
<!-- 点击重试时,触发 retry 事件,Vue 会自动重新加载组件 -->
<button @click="$emit('retry')">重试</button>
</div>
</template>
<style scoped>
.error {
text-align: center;
padding: 40px;
color: red;
}
</style>
5.4 三个 Tab 页面组件(简单示例)
HomeTab.vue
vue
<template>
<div>
<h2>首页</h2>
<p>这是首页内容,首次进入时异步加载。</p>
<!-- 模拟一个输入框,测试 KeepAlive 缓存 -->
<input v-model="text" placeholder="输入点东西,切走再回来看还在不在" />
</div>
</template>
<script setup>
import { ref } from 'vue'
const text = ref('')
</script>
DiscoverTab.vue 和 ProfileTab.vue 类似,只是标题不同。
5.5 TabBar 组件
vue
<!-- TabBar.vue -->
<template>
<div class="tab-bar">
<!-- 遍历 tabs 数据,渲染按钮 -->
<button
v-for="tab in tabs"
:key="tab.name"
:class="{ active: current === tab.comp }"
@click="$emit('change', tab.comp)"
>
{{ tab.label }}
</button>
</div>
</template>
<script setup>
defineProps({
tabs: Array, // [{ label: '首页', comp: HomeComp }, ...]
current: Object // 当前选中的组件对象
})
defineEmits(['change'])
</script>
<style scoped>
.tab-bar {
display: flex;
justify-content: space-around;
border-top: 1px solid #eee;
padding: 10px;
}
.tab-bar button {
border: none;
background: none;
cursor: pointer;
padding: 5px 15px;
}
.tab-bar button.active {
color: #409eff;
font-weight: bold;
}
</style>
5.6 App.vue 主组件
vue
<template>
<div class="app">
<!-- 主内容区:动态组件 + 缓存 -->
<KeepAlive>
<component :is="currentTab" />
</KeepAlive>
<!-- 底部导航栏 -->
<TabBar :tabs="tabs" :current="currentTab" @change="switchTab" />
</div>
</template>
<script setup>
import { ref, defineAsyncComponent } from 'vue'
import TabBar from './components/TabBar.vue'
import LoadingSpinner from './components/LoadingSpinner.vue'
import ErrorDisplay from './components/ErrorDisplay.vue'
// 定义异步组件,带 loading 和 error 处理
const HomeTab = defineAsyncComponent({
loader: () => import('./components/tabs/HomeTab.vue'),
loadingComponent: LoadingSpinner,
delay: 200,
errorComponent: ErrorDisplay,
timeout: 5000
})
const DiscoverTab = defineAsyncComponent({
loader: () => import('./components/tabs/DiscoverTab.vue'),
loadingComponent: LoadingSpinner,
delay: 200,
errorComponent: ErrorDisplay,
timeout: 5000
})
const ProfileTab = defineAsyncComponent({
loader: () => import('./components/tabs/ProfileTab.vue'),
loadingComponent: LoadingSpinner,
delay: 200,
errorComponent: ErrorDisplay,
timeout: 5000
})
// 当前选中的 Tab 组件
const currentTab = ref(HomeTab)
// Tab 数据,传给底部导航栏
const tabs = [
{ label: '首页', comp: HomeTab },
{ label: '发现', comp: DiscoverTab },
{ label: '我的', comp: ProfileTab }
]
// 切换 Tab
function switchTab(comp) {
currentTab.value = comp
}
</script>
<style scoped>
.app {
display: flex;
flex-direction: column;
height: 100vh;
}
.app > :first-child {
flex: 1;
overflow-y: auto;
}
</style>
运行效果:
-
首次打开只加载首页 Tab 代码,点击"发现"或"我的"时才去下载对应代码,下载时显示 loading。
-
如果网络故障加载失败,显示错误页,可点击重试。
-
在首页输入框里打字,切换到发现再切回来,文字还在(KeepAlive 缓存)。
六、总结
今天我们学到了:
-
动态组件
<component :is="...">:一个标签代替一堆v-if,根据数据切换组件。 -
KeepAlive 配合动态组件:缓存组件状态,避免重复创建销毁。
-
异步组件
defineAsyncComponent:按需加载组件代码,提升首屏速度。 -
异步组件的 loading/error 处理:给用户友好的加载和失败提示。
这三个技能组合起来,能让你写出既快又流畅的单页应用。尤其是做移动端 H5 或后台管理系统,异步组件几乎是标配。
有问题评论区说,我挨个回。下篇见!