Vue动态组件+异步组件实战:Tab切换、按需加载、KeepAlive缓存,一次搞定

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

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

相关推荐
风骏时光牛马1 小时前
Stylus预处理器完整语法与项目实战详细代码案例
前端
tangdou3690986551 小时前
DevOps Skill工具链:CI/CD流水线搭建全攻略
前端
tangdou3690986551 小时前
前端Skill全家桶:React+Vue+TypeScript开发实战
前端
大大杰哥1 小时前
Vue2学习(3)--组件中的通信方式/组件之间的交互
java·前端·javascript
糖醋丸子1 小时前
D3生成topo 结点连线 webpack 配置兼容ie 11
前端
阿猫的故乡1 小时前
Vue3自定义插件:封装一个全局消息提示插件,所有组件都能直接用
前端·javascript·vue.js
用户83134859306981 小时前
Cesium实现实时联动鹰眼缩略图
vue.js·cesium
橘子星1 小时前
树与二叉树:从概念到 JavaScript 实现
前端·javascript·面试
小小高不懂写代码1 小时前
Transformer与注意力机制
前端·人工智能