一、先搞懂,为啥路由切换会丢状态?
做Vue项目时,有没有遇到过这样的情况?填了一半的表单,切换路由再回来全没了;列表页划到下面,返回后又回到顶部……路由切换时组件状态丢失,确实影响用户体验,那在Vue Router里,到底怎么让页面“之前的状态?今天就把常用方法拆明白,从基础到进阶一次讲透~
Vue里,组件的状态和它的实例是绑定的,当路由切换时,如果两个路由对应的组件不一样,Vue会销毁旧组件实例,然后创建新的组件实例,组件一旦被销毁,它内部存储的数据、DOM结构(比如滚动条位置)自然就跟着消失了。
举个例子:/page1
对应ComponentA
,/page2
对应ComponentB
,从/page1
切到/page2
时,ComponentA
的实例会被销毁;等切回/page1
,Vue又会重新创建ComponentA
的实例——之前填的表单内容、滚动过的位置,自然就全重置了,这就是“状态丢失”的根本原因。
最常用:用<keep - alive>给组件“续命”
要解决组件实例被销毁的问题,Vue官方提供了一个内置组件<keep - alive>——它能帮我们缓存组件实例,让组件切换时不销毁,而是“休眠”在内存里,下次再进入该组件时,直接复用之前的实例,状态自然就保留下来了。
给路由出口穿“缓存衣”
想让路由对应的组件被缓存,只需要在路由出口(<router - view>)外面包一层<keep - alive>,比如在项目的根组件(App.vue)里这么写:
<template> <div class="app"> <keep - alive> <router - view></router - view> </keep - alive> </div> </template>
这样一来,所有通过<router - view>渲染的组件,默认都会被缓存,但注意:如果所有页面都无条件缓存,可能会导致内存占用过高(尤其是页面多、组件复杂时),所以实际开发中,我们需要精细化控制哪些组件该缓存。
精准控制:哪些组件该缓存?
控制缓存范围有两种常见思路:按组件名匹配 和 结合路由元信息(meta)。
① 按组件名匹配(include/exclude)
<keep - alive>提供了include
和exclude
属性,用来指定“哪些组件要缓存”“哪些组件不缓存”,它们的值是组件名的字符串(多个用逗号分隔)。
只希望缓存名为FormPage
和ListPage
的组件:
<keep - alive include="FormPage,ListPage"> <router - view></router - view> </keep - alive>
这里需要注意:组件名要和组件定义时的name
选项一致,比如组件要这样写:
export default { name: 'FormPage', // 必须和include里的名称对应 // ...其他逻辑 }
② 结合路由元信息(meta)
这种方式更灵活——我们可以在路由配置里给需要缓存的页面“贴标签”,然后在<keep - alive>里根据标签判断是否缓存。
第一步:在路由配置中给路由加meta.keepAlive
标记。
const routes = [ { path: '/form', component: FormPage, meta: { keepAlive: true } // 标记该页面需要缓存 }, { path: '/list', component: ListPage, meta: { keepAlive: true } }, { path: '/detail', component: DetailPage, meta: { keepAlive: false } // 标记该页面不需要缓存 } ]
第二步:在<keep - alive>中根据$route.meta.keepAlive
判断是否缓存当前路由组件:
<keep - alive> <router - view v - if="$route.meta.keepAlive"></router - view> </keep - alive> <router - view v - else></router - view>
这样一来,只有meta.keepAlive
为true
的路由组件会被缓存,避免“无差别缓存”导致的性能问题。
缓存后,组件生命周期咋变?
普通组件的生命周期是:进入时执行created → mounted
,离开时执行beforeDestroy → destroyed
,但被<keep - alive>缓存后,组件的生命周期会变成:
- 离开时(组件被缓存,不销毁):触发
deactivated
; - 再次进入时(复用缓存的实例,不重新创建):触发
activated
。
如果你想在“组件离开时保存状态,进入时恢复状态”,就得把逻辑写到deactivated
和activated
里。
举个例子:表单组件离开时保存数据,进入时恢复数据:
export default { data() { return { formData: { name: '', age: '' } } }, deactivated() { // 离开时,把表单数据存到localStorage(也可以存到Vuex/Pinia) localStorage.setItem('formCache', JSON.stringify(this.formData)) }, activated() { // 进入时,从localStorage恢复数据 const cache = localStorage.getItem('formCache') if (cache) { this.formData = JSON.parse(cache) } } }
踩坑提醒:缓存后数据不更新?
比如有个列表页用了缓存,后端数据更新后,组件实例没被销毁,所以不会重新请求数据——这就会导致“页面数据还是旧的”。
解决方法很简单:在activated
生命周期里主动刷新数据。
export default { data() { return { list: [] } }, methods: { fetchData() { // 调用接口获取最新列表数据 axios.get('/api/list').then(res => { this.list = res.data }) } }, activated() { this.fetchData() // 每次进入组件(从缓存唤醒时),主动请求最新数据 } }
如果想“强制组件销毁重建”(比如某些极端场景必须重置状态),可以给<router - view>加key
属性,让Vue认为“组件变了”,从而销毁旧实例、创建新实例,但这种做法会失去缓存的意义,所以要谨慎使用:
<router - view :key="$route.fullPath"></router - view>
路由元信息(meta):给路由“贴标签”管理缓存
路由元信息(meta
)是Vue Router里的一个自定义配置字段——我们可以在每个路由的配置中加一个meta
对象,存各种自定义信息(比如页面标题、是否需要缓存、权限要求等),这里重点讲它在“缓存管理”中的作用。
场景:列表页和详情页
假设我们有个“商品列表页”和“商品详情页”:
- 列表页需要保持滚动位置和筛选条件(切换路由后回来,希望还是之前的状态);
- 详情页不需要缓存(每次进入都是新的商品信息,状态重置也没关系)。
这时候,用路由meta
标记就很方便:
const routes = [ { path: '/product - list', component: ProductList, meta: { keepAlive: true, title: '商品列表' } // 列表页需要缓存 }, { path: '/product - detail/:id', component: ProductDetail, meta: { keepAlive: false, title: '商品详情' } // 详情页不需要缓存 } ]
动态判断缓存逻辑
有时候需求更复杂——列表页从详情页返回时要缓存,从其他页面进入时不缓存”,这时候可以用路由守卫动态修改meta.keepAlive
的值。
举个例子,在路由全局前置守卫(router.beforeEach
)里判断:
router.beforeEach((to, from) => { // 如果要进入的是商品列表页,且从商品详情页过来 if (to.name === 'ProductList' && from.name === 'ProductDetail') { to.meta.keepAlive = true // 允许缓存 } else { to.meta.keepAlive = false // 不允许缓存 } })
这样就能根据“从哪个页面跳转过来”,灵活控制列表页是否缓存,满足复杂业务逻辑。
Vuex/Pinia:全局存状态,组件销毁也不怕
如果状态需要跨组件共享(比如多个组件要用同一份数据),或者希望持久化(比如刷新页面后状态也不丢失),那用状态管理库(Vuex或Pinia)会更靠谱。
原理很简单:把状态存在全局Store里,不管组件是否销毁,Store里的数据一直存在,组件创建时从Store取数据,销毁前把数据存回Store即可。
Pinia示例:持久化表单数据
下面用Pinia(Vuex同理)演示“表单数据跨路由保持”的逻辑。
第一步:定义Store(存储表单数据),新建文件stores/form.js
:
import { defineStore } from 'pinia' <p>export const useFormStore = defineStore('form', { state: () => ({ name: '', // 表单字段:姓名 age: '' // 表单字段:年龄 }), actions: { // 保存表单数据到Store setForm(data) { this.name = data.name this.age = data.age }, // 从Store获取表单数据 getForm() { return { name: this.name, age: this.age } } } })
第二步:在表单组件中使用Store,组件创建时从Store取数据,销毁前把数据存回Store:
<template> <div> <input v - model="localName" placeholder="姓名" /> <input v - model="localAge" placeholder="年龄" /> </div> </template> <p><script setup> import { useFormStore } from '@/stores/form' import { onBeforeUnmount, ref } from 'vue'</p> <p>// 拿到FormStore实例 const formStore = useFormStore()</p> <p>// 组件创建时,从Store取数据 const localName = ref(formStore.name) const localAge = ref(formStore.age)</p> <p>// 组件销毁前,把数据存回Store onBeforeUnmount(() => { formStore.setForm({ name: localName.value, age: localAge.value }) }) </script>
这样一来,不管路由怎么切换,Store里的状态始终保留,就算组件被销毁重建,下次创建时也能从Store拿到最新的表单数据。
进阶:结合localStorage持久化
如果希望“刷新页面后,状态也不丢失”,可以给Store加本地存储(localStorage)的逻辑,修改上面的Store:
import { defineStore } from 'pinia' <p>export const useFormStore = defineStore('form', { state: () => ({ // 从localStorage取数据,没有则用默认值 name: localStorage.getItem('formName') || '', age: localStorage.getItem('formAge') || '' }), actions: { setForm(data) { this.name = data.name this.age = data.age // 同步把数据存到localStorage localStorage.setItem('formName', data.name) localStorage.setItem('formAge', data.age) } } })
这样,状态不仅能在“路由切换、组件销毁”时保留,就算用户刷新页面,数据也会从localStorage恢复——适合“登录态”“购物车”这类关键且需要持久化的信息。
特殊场景:手动缓存组件实例/滚动位置
有些场景下,<keep - alive>
和Store也搞不定,得手动处理,比如下面这两种情况:
保持滚动位置
列表页划到第10屏,返回后要回到原来的位置,这时候可以监听路由切换,保存/恢复滚动位置:
<script> export default { data() { return { scrollMap: {} // 存每个页面的滚动位置(key是路由path,value是滚动条位置) } }, watch: { // 路由切换时,保存离开页面的滚动位置 '$route'(to, from) { this.scrollMap[from.path] = window.scrollY } }, activated() { // 进入页面时,恢复滚动条位置 if (this.scrollMap[this.$route.path]) { window.scrollTo(0, this.scrollMap[this.$route.path]) } } } </script>
注意:这个方法要配合<keep - alive>使用——因为activated
是“被缓存的组件”才有的生命周期,如果组件没被缓存,activated
不会触发,滚动位置也就无法恢复。
第三方组件状态(比如地图、富文本)
有些第三方组件(比如地图、富文本编辑器)初始化特别耗时,切换路由后,希望保留它们的实例,避免重复初始化,这时候可以:
- 用<keep - alive>直接缓存整个组件(最简单,但可能缓存过多内容);
- 在组件外手动建一个“容器”保存实例,比如地图组件:
// 全局变量,保存地图实例
let mapInstance = null
export default {
mounted() {
if (!mapInstance) {
// 第一次进入,初始化地图
mapInstance = new Map({
container: this.$refs.mapContainer,
// ...其他配置
})
} else {
// 复用已有
版权声明
本文仅代表作者观点,不代表Code前端网立场。
本文系作者Code前端网发表,如需转载,请注明页面地址。
发表评论:
◎欢迎参与讨论,请在这里发表您的看法、交流您的观点。