Code前端首页关于Code前端联系我们

一、先搞懂,为啥路由切换会丢状态?

terry 2个月前 (08-11) 阅读数 83 #Vue

做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>提供了includeexclude属性,用来指定“哪些组件要缓存”“哪些组件不缓存”,它们的值是组件名的字符串(多个用逗号分隔)

只希望缓存名为FormPageListPage的组件:

<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.keepAlivetrue的路由组件会被缓存,避免“无差别缓存”导致的性能问题。

缓存后,组件生命周期咋变?

普通组件的生命周期是:进入时执行created → mounted,离开时执行beforeDestroy → destroyed,但被<keep - alive>缓存后,组件的生命周期会变成:

  • 离开时(组件被缓存,不销毁):触发deactivated
  • 再次进入时(复用缓存的实例,不重新创建):触发activated

如果你想在“组件离开时保存状态,进入时恢复状态”,就得把逻辑写到deactivatedactivated里。

举个例子:表单组件离开时保存数据,进入时恢复数据:

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前端网发表,如需转载,请注明页面地址。

发表评论:

◎欢迎参与讨论,请在这里发表您的看法、交流您的观点。

热门