Vue动态组件+异步组件实战:Tab切换、按需加载、KeepAlive缓存,一次搞定
2026/6/16 3:41:13 网站建设 项目流程

一、从 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 或后台管理系统,异步组件几乎是标配。

有问题评论区说,我挨个回。下篇见!

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询