Vue动态组件+异步组件实战:Tab切换、按需加载、KeepAlive缓存,一次搞定
一、从 Tab 切换说起假设你接到个需求做一个后台管理页面左侧菜单点击后右侧内容区跟着变。菜单有好几项每个菜单对应一个组件。你可能会想用v-if/v-else一个一个判断vuetemplate div button clickcurrent home首页/button button clickcurrent users用户管理/button button clickcurrent settings系统设置/button Home v-ifcurrent home / Users v-else-ifcurrent users / Settings v-else-ifcurrent settings / /div /template如果只有三两个还好后面菜单加到十几个就要写一长串v-if又丑又不好维护。这时候就该动态组件出场了。二、动态组件一个标签搞定组件切换Vue 内置了一个component标签专门用来做动态组件。它有一个is属性指向哪个组件就渲染哪个组件。2.1 基础用法vuetemplate div !-- 三个按钮点击改变 currentComponent 的值 -- button clickcurrentComponent Home首页/button button clickcurrentComponent Users用户/button button clickcurrentComponent Settings设置/button !-- 动态组件标签component :is组件名 / 当 currentComponent 是 Home 时渲染 Home 组件 当 currentComponent 是 Users 时渲染 Users 组件 -- component :iscurrentComponent / /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 :isxxx里的xxx可以是组件的名字字符串也可以是组件对象本身。上面我们传的是字符串HomeVue 会自动去找同名的组件。但更推荐传组件对象本身因为这样不用关心名字映射。2.2 传组件对象的方式更稳妥vuetemplate div button clickcurrent homeComp首页/button button clickcurrent usersComp用户/button button clickcurrent settingsComp设置/button !-- 直接传组件对象current 是哪个组件就渲染哪个 -- component :iscurrent / /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包一层。vuetemplate div button clickcurrent homeComp首页/button button clickcurrent usersComp用户/button button clickcurrent settingsComp设置/button !-- KeepAlive 包裹动态组件离开时组件不销毁状态还在 -- KeepAlive component :iscurrent / /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 定义异步组件vuescript setup import { defineAsyncComponent } from vue // 定义一个异步组件参数是一个函数返回 import() const AsyncHome defineAsyncComponent(() import(./Home.vue)) const AsyncUsers defineAsyncComponent(() import(./Users.vue)) const AsyncSettings defineAsyncComponent(() import(./Settings.vue)) /scriptdefineAsyncComponent的作用接收一个返回 Promise 的函数这里是import()组件真正需要渲染时才执行这个函数下载代码。4.2 异步组件 动态组件 KeepAlive 组合vuetemplate div button clickcurrent asyncHome首页/button button clickcurrent asyncUsers用户/button button clickcurrent asyncSettings设置/button KeepAlive component :iscurrent / /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 状态。javascriptconst 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 classloading span加载中请稍候.../span /div /template自定义 Error 组件示例vue!-- ErrorDisplay.vue -- template div classerror p组件加载失败/p button click$emit(retry)点击重试/button /div /template然后在配置里javascriptconst 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 项目结构textsrc/ ├── components/ │ ├── TabBar.vue # 底部导航栏 │ ├── LoadingSpinner.vue # 通用 loading 组件 │ ├── ErrorDisplay.vue # 通用 error 组件 │ └── tabs/ │ ├── HomeTab.vue # 首页组件 │ ├── DiscoverTab.vue # 发现组件 │ └── ProfileTab.vue # 我的组件 └── App.vue5.2 通用 Loading 组件vue!-- LoadingSpinner.vue -- template div classspinner span 加载中.../span /div /template style scoped .spinner { text-align: center; padding: 40px; color: #999; } /style5.3 通用 Error 组件vue!-- ErrorDisplay.vue -- template div classerror p 加载失败/p !-- 点击重试时触发 retry 事件Vue 会自动重新加载组件 -- button click$emit(retry)重试/button /div /template style scoped .error { text-align: center; padding: 40px; color: red; } /style5.4 三个 Tab 页面组件简单示例HomeTab.vuevuetemplate div h2首页/h2 p这是首页内容首次进入时异步加载。/p !-- 模拟一个输入框测试 KeepAlive 缓存 -- input v-modeltext placeholder输入点东西切走再回来看还在不在 / /div /template script setup import { ref } from vue const text ref() /scriptDiscoverTab.vue 和 ProfileTab.vue 类似只是标题不同。5.5 TabBar 组件vue!-- TabBar.vue -- template div classtab-bar !-- 遍历 tabs 数据渲染按钮 -- button v-fortab in tabs :keytab.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; } /style5.6 App.vue 主组件vuetemplate div classapp !-- 主内容区动态组件 缓存 -- KeepAlive component :iscurrentTab / /KeepAlive !-- 底部导航栏 -- TabBar :tabstabs :currentcurrentTab changeswitchTab / /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 或后台管理系统异步组件几乎是标配。有问题评论区说我挨个回。下篇见