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

Vue3 onBeforeRouteUpdate是什么?怎么用?有什么坑要避?

terry 51分钟前 阅读数 14 #Vue

最近刷前端社区,看到不少人在讨论路由跳转时组件复用的问题——比如从“用户详情页/1”跳到“用户详情页/2”,页面参数变了但DOM没刷新、数据没加载,甚至连之前填的临时表单内容都留在那里,很多老Vue2开发者下意识想找keep-alive的钩子,或者直接给组件加个key强制销毁重建,但其实Vue3和Vue Router 4早就给了更优雅的解决方案:onBeforeRouteUpdate。

先搞懂onBeforeRouteUpdate到底解决什么问题

在正式讲用法之前,得先弄明白它的“出现场景”和“核心定位”。

首先回忆一下Vue Router的组件复用规则:当两个路由匹配到同一个组件时,Vue会复用该组件的实例,而不是销毁旧的、创建新的,这么做的好处很明显——性能更好,尤其是组件内部有复杂DOM、动画或临时状态不需要清空的时候,复用能省不少渲染时间和内存,但坏处就是刚才说的那些:组件的created、mounted这些生命周期钩子不会再触发,导致依赖路由参数的请求、状态初始化都不会执行,页面看起来就像“卡壳”了一样。

keep-alive的钩子比如activated虽然也能检测复用后的激活,但它的触发范围更广——不管是路由参数变了,还是从别的组件切回来(不管是不是同路由),都会触发activated,如果只想在同组件路由参数/query/hash变化时做处理,比如只请求新用户数据,不管切回来其他临时状态,那activated就有点“大材小用”,甚至可能带来逻辑混乱(比如切回来时重复请求或者重置了不该重置的东西)。

这时候onBeforeRouteUpdate就派上用场了:它是Vue Router 4专属的、用于同组件内路由更新前执行的组合式API钩子(Vue2对应的是beforeRouteUpdate选项式API,但现在组合式API是主流),可以精准地在路由参数变化、即将复用组件实例的时候,处理数据更新、临时状态保存/重置等逻辑。

一步步教你组合式API里用onBeforeRouteUpdate

既然是组合式API,那肯定得先在setup里引入它,对吧?

第一步:从vue-router导入

使用前记得确保项目里装的是Vue Router 4.x版本(和Vue3配套的必须是这个,装错了根本找不到这个钩子),然后在要处理路由更新的组件里,从vue-router解构出onBeforeRouteUpdate:

import { onBeforeRouteUpdate } from 'vue-router'

第二步:在setup里注册钩子

onBeforeRouteUpdate的回调函数接收三个参数,和Vue2的选项式API钩子参数一致,分别是:

  • to:即将进入的目标路由对象
  • from:当前正要离开的路由对象
  • next:一个可选的回调函数(不过Vue Router 4推荐用返回值替代next,更符合组合式API的异步处理习惯)

回调函数里可以写同步或异步代码,比如请求新数据、重置表单、保存滚动条位置(不过滚动条位置通常用router的scrollBehavior配置更方便,但这里也能做补充)。

举个最常用的例子:同用户详情页切换用户ID时重新请求数据。

import { ref, onMounted } from 'vue'
import { onBeforeRouteUpdate, useRoute } from 'vue-router'
// 假设这是一个请求用户详情的函数
import { fetchUserDetail } from '@/api/user'
export default {
  setup() {
    const route = useRoute()
    const userDetail = ref({})
    const loading = ref(false)
    // 初始加载时用onMounted请求数据
    const loadUser = async (id) => {
      loading.value = true
      try {
        const res = await fetchUserDetail(id)
        userDetail.value = res.data
      } finally {
        loading.value = false
      }
    }
    onMounted(() => {
      loadUser(route.params.id)
    })
    // 路由参数变化时用onBeforeRouteUpdate请求新数据
    onBeforeRouteUpdate(async (to) => {
      // 这里可以加个判断,只有当用户ID真的变了才请求,防止不必要的渲染(不过同组件匹配时params一般是变的,但加个更稳妥)
      if (to.params.id!== route.params.id) {
        await loadUser(to.params.id)
      }
    })
    return { userDetail, loading }
  }
}

这里要注意,useRoute返回的route对象是响应式的,理论上可以用watch监听route.params或者route.query来实现数据更新,但onBeforeRouteUpdate有个不可替代的优势:它是在导航确认前执行的,而且可以控制导航的行为(比如用返回false取消导航,或者返回一个新的路由对象重定向),而watch是在导航完成后、DOM更新前执行的,没法干预导航本身。

比如我们可以加个逻辑:如果新的用户ID不存在,直接取消导航,提示用户错误:

onBeforeRouteUpdate(async (to) => {
  if (to.params.id!== route.params.id) {
    try {
      await loadUser(to.params.id)
    } catch (err) {
      // 假设接口返回404时会抛出错误
      alert('该用户不存在')
      // 返回false取消导航
      return false
    }
  }
})

如果用watch的话,这时候导航已经完成了,只能在提示后再手动跳回去,体验会差很多。

第三步:处理异步逻辑的next替代方案

刚才说了Vue Router 4推荐用返回值替代next,那具体怎么替代呢?

  • 返回undefined或true:导航继续,和next()一样
  • 返回false:导航取消,和next(false)一样
  • 返回一个路由对象(字符串、对象都行):重定向到该路由,和next('/xxx')或next({ path: '/xxx' })一样
  • 返回一个Promise:Promise resolve时导航继续(如果resolve的是上面三种值之一,就按对应规则处理),Promise reject时导航取消,同时会触发router.onError的回调

这个替代方案确实更简洁,尤其是在处理异步逻辑的时候,不用再考虑next的调用顺序问题(Vue2里如果异步代码里没正确调用next,导航会一直卡住)。

避坑指南:这几个点90%的开发者都会踩

虽然onBeforeRouteUpdate看起来简单,但实际开发中还是有不少坑的,我整理了几个最常见的:

坑1:在setup顶层直接调用useRoute后,以为onBeforeRouteUpdate里能直接拿到最新的to参数?

不对!虽然useRoute返回的route是响应式的,但在onBeforeRouteUpdate的回调函数执行时,useRoute的route对象还没更新——因为导航还没确认,路由状态还没切换,所以要在回调里用to参数,而不是直接用route.params/query/hash,否则你拿到的还是旧的参数,数据更新自然不会生效。

刚才的例子里我已经加了正确的判断,但还是有很多人会忘,比如写成:

// 错误写法!
onBeforeRouteUpdate(async () => {
  await loadUser(route.params.id) // 这里拿到的是旧的ID!
})

这个一定要注意。

坑2:只写了onBeforeRouteUpdate,忘了写onMounted?

这也是新手常犯的错误:以为onBeforeRouteUpdate会在第一次加载组件时触发,错!onBeforeRouteUpdate只有在同组件内路由更新时才会触发,第一次进入该组件时,触发的是onBeforeRouteEnter(组合式API里也有这个钩子,但它的回调函数里不能直接用setup的上下文,因为组件实例还没创建,得用回调里的第三个参数vm,但那是选项式API的写法,现在组合式API处理第一次加载还是用onMounted更方便)。

所以正确的做法是:第一次加载用onMounted,路由参数变化用onBeforeRouteUpdate,两个钩子配合使用,才能保证不管是第一次进还是切换参数,数据都能正常加载。

坑3:在onBeforeRouteUpdate里修改了响应式数据,但DOM没更新?

只要你修改的是ref或reactive定义的响应式数据,DOM是会自动更新的,但如果没更新,可能有两个原因:

  1. 你修改的不是响应式数据(这个不用多说,检查一下就行)
  2. 你在回调函数里用了同步代码,但修改数据后没有触发Vue的响应式更新?不对,同步修改响应式数据肯定会触发,除非你做了什么奇怪的操作,比如直接替换了ref的.value但被Vue的响应式系统“忽略”了(比如直接赋值了一个新对象的引用,但旧对象的引用还在其他地方?不对,Vue3的Proxy是监听对象本身的,替换ref的.value肯定没问题) 哦,对了,还有一种可能:你在onBeforeRouteUpdate里用了next回调函数(虽然不推荐,但还是可以用的),但next的调用顺序不对——比如先修改了数据,再调用next()?不对,这应该没问题,或者先调用了next(),再修改数据?这时候导航已经确认了,DOM可能已经更新过了,但修改数据还是会触发后续的DOM更新的。

可能更常见的原因是:你在判断条件里写错了,导致回调函数根本没执行,比如刚才的例子里,你判断了to.params.id!== route.params.id,但如果路由参数是query而不是params,你可能忘了改成to.query.id!== route.query.id?或者路由参数的名字写错了?这个可以在回调函数里加个console.log(to, from),看看参数是不是真的变了,回调是不是真的执行了。

坑4:在onBeforeRouteUpdate里用了onBeforeRouteEnter的vm参数?

组合式API里的onBeforeRouteEnter确实有第三个参数vm,但onBeforeRouteUpdate没有——因为onBeforeRouteUpdate是组件实例已经存在的时候触发的,你可以直接用setup里的响应式数据和方法,不用vm。

坑5:多个同组件嵌套路由时,onBeforeRouteUpdate的触发时机?

比如有这样的嵌套路由:

const routes = [
  {
    path: '/user/:id',
    component: UserLayout,
    children: [
      {
        path: 'profile',
        component: UserProfile
      },
      {
        path: 'posts',
        component: UserPosts
      }
    ]
  }
]

这时候如果从“/user/1/profile”跳到“/user/1/posts”,UserLayout和UserProfile/UserPosts都是复用的吗?UserLayout是复用的(因为path的前缀还是/user/:id,而且id没变),UserProfile会被销毁,UserPosts会被创建,所以UserLayout的onBeforeRouteUpdate会不会触发?

会的!因为虽然UserLayout的params没变,但它的子路由变了,也就是路由的matched数组变了,所以Vue Router也会认为这是同组件内的路由更新,会触发UserLayout的onBeforeRouteUpdate,这时候可以在回调里判断一下to.matched和from.matched的区别,比如只有当子路由变了的时候,才做一些布局上的调整,而不是请求数据。

进阶用法:配合useRouter做更多操作

除了处理数据更新和控制导航,onBeforeRouteUpdate还可以配合useRouter做更多操作,

保存/恢复表单的临时状态

比如在用户编辑页,用户填了一半表单,突然切换到另一个用户的编辑页,这时候可以在onBeforeRouteUpdate里保存当前表单的临时状态,切换回来的时候再恢复(不过如果是不同用户的话,可能不需要恢复,但如果是误操作切换回来,或者需要对比两个用户的表单,就可以这么做)。

不过这里要注意,onBeforeRouteUpdate是在导航确认前执行的,所以如果保存临时状态需要异步操作(比如保存到localStorage没问题,同步的;但如果保存到后端,就需要等待异步操作完成),否则导航可能已经完成了,临时状态还没保存好。

记录用户的浏览历史

比如在用户详情页,用户连续浏览了多个用户,这时候可以在onBeforeRouteUpdate里把每个浏览过的用户ID保存到一个数组里,然后显示在页面的“最近浏览”栏里。

结合pinia/vuex管理全局状态

比如在电商网站的商品详情页,用户切换商品时,把当前商品的ID保存到pinia的全局状态里,方便其他组件(比如购物车、收藏夹)使用。

什么时候用onBeforeRouteUpdate,什么时候用watch/keep-alive?

最后再来总结一下三个方案的适用场景,方便大家选择:

  1. onBeforeRouteUpdate:适用于同组件内路由参数/query/hash/matched变化时,需要处理数据更新、控制导航(取消、重定向)、保存临时状态等逻辑的情况,这是最精准、最优雅的方案。
  2. watch(route):适用于同组件内路由参数变化时,只需要处理数据更新、不需要控制导航的情况,这个方案比onBeforeRouteUpdate简单,但不够灵活。
  3. keep-alive + activated:适用于需要保留组件的所有临时状态(比如表单内容、滚动条位置、动画状态),不管是从同组件路由切换回来,还是从其他组件切换回来的情况。
  4. 给组件加key强制销毁重建:适用于不需要保留组件的任何临时状态,而且组件内部逻辑比较简单,销毁重建的性能损失可以忽略不计的情况,这个方案最简单,但性能最差,而且会丢失所有临时状态。

优先用onBeforeRouteUpdate,其次用watch(route),如果需要保留临时状态再用keep-alive + activated,最后才考虑加key强制销毁重建。

好啦,关于Vue3 onBeforeRouteUpdate的内容就讲到这里啦,希望能帮到大家!如果还有其他问题,欢迎在评论区留言讨论~

版权声明

本文仅代表作者观点,不代表Code前端网立场。
本文系作者Code前端网发表,如需转载,请注明页面地址。

热门