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

一、先想清楚,为什么要监听路由变化?

terry 8小时前 阅读数 10 #Vue

做Vue项目时,经常碰到“路由变了要执行逻辑”的需求——比如进入页面要埋点统计、参数变了要重新请求数据、离开页面要做权限校验…那vue router里怎么监听路由变化(也就是“on change”的逻辑)?不同场景下有哪些实现方式?踩过哪些坑?今天从全局到组件、从基础到实战,把这些事儿掰碎了讲明白~

先聊聊实际开发中常见的场景,你肯定碰到过这些需求:

  • 权限校验:进入订单详情页前,得判断用户是否登录、有没有权限,这时候要在路由变化前拦截;
  • 数据更新:列表页路由带了?page=2,参数变了就得重新调接口拉第二页数据;
  • 埋点统计:用户从首页跳到列表页,要记录“页面跳转”行为,统计停留时长;
  • 状态重置:离开表单页时,把未提交的临时数据清掉,避免下次进入有残留;
  • 动画控制:路由切换时触发页面过渡动画,比如左滑、右滑效果;

这些场景都得“感知路由变化”,再执行对应的逻辑,接下来分全局层面组件内部两个维度,讲具体怎么实现。

全局层面:用导航守卫监听路由变化

Vue Router提供了导航守卫,能在路由跳转的“生命周期”不同阶段插入逻辑,常见的有beforeEach(跳转前)、afterEach(跳转后)、beforeResolve(组件解析前)这些,还有处理错误的onError

全局前置守卫:router.beforeEach

作用:路由跳转之前触发,能决定“是否允许跳转”(比如权限拦截)。
用法示例(判断用户是否登录):

// router/index.js 里配置
const router = createRouter({...})
router.beforeEach((to, from, next) => {
  // to:要跳转到的目标路由;from:当前离开的路由;next:决定是否跳转的函数
  const isLogin = localStorage.getItem('token') // 假设用localStorage存登录态
  if (to.meta.requiresAuth && !isLogin) { 
    // 如果目标路由需要权限,且用户没登录,强制跳登录页
    next({ name: 'Login' })
  } else {
    next() // 允许跳转
  }
})

适用场景:权限控制、登录拦截、全局loading启动(比如跳转前显示加载动画)。

全局后置钩子:router.afterEach

作用:路由跳转完成后触发(此时组件已经渲染),不能拦截跳转,适合做“不影响跳转”的逻辑(比如埋点、关闭loading)。
用法示例(页面跳转埋点):

router.afterEach((to, from) => {
  // 统计页面跳转:记录从from.path到to.path的行为
  analytics.track('page_leave', { from: from.path })
  analytics.track('page_enter', { to: to.path })
})

适用场景:埋点统计、页面标题修改(document.title = to.meta.title)、关闭全局加载动画。

其他全局守卫:beforeResolve & onError

  • router.beforeResolve:和beforeEach类似,但在组件解析之后(比如异步组件加载完)、beforeEach之后触发,适合做“最后一次拦截”;
  • router.onError:监听路由跳转时的错误(比如异步组件加载失败、路由配置错误),示例:
router.onError((error) => {
  console.error('路由跳转出错:', error)
  // 可以跳转到错误页
  router.push({ name: 'ErrorPage' })
})

这些全局守卫的逻辑,是“不管哪个路由变化,都会触发”,适合全局统一的逻辑(比如权限、埋点、错误处理)。

组件内部:监听$route的变化

如果只是某个组件需要感知路由变化(比如列表页根据query.page重新请求数据),用组件内的watch监听$route更灵活。

监听整个$route对象变化

Vue2和Vue3的写法差不多,在组件的watch里加逻辑:

Vue3 组合式API写法

<template>...</template>
<script setup>
import { watch } from 'vue'
import { useRoute } from 'vue-router'
const route = useRoute()
watch(route, (newRoute, oldRoute) => {
  // 路由变化时执行,比如newRoute是新路由,oldRoute是旧路由
  console.log('路由变了:', newRoute.path, oldRoute.path)
  // 假设是列表页,根据newRoute.query.page请求数据
  fetchData(newRoute.query.page)
}, { immediate: true }) // immediate:组件创建时立即执行一次(可选)
</script>

Vue2 选项式API写法

<script>
export default {
  watch: {
    $route(newRoute, oldRoute) {
      // 逻辑同上
    }
  }
}
</script>

适用场景:组件内依赖路由参数变化,比如详情页/product/:id,id变了要重新请求商品数据。

监听路由的特定属性(比如queryparams

如果只关心queryparams的变化,不用监听整个$route,可以更精准:

<script setup>
import { watch } from 'vue'
import { useRoute } from 'vue-router'
const route = useRoute()
// 只监听query变化
watch(() => route.query, (newQuery, oldQuery) => {
  console.log('查询参数变了:', newQuery, oldQuery)
  // 比如搜索页,query里的keyword变了,重新搜索
  searchData(newQuery.keyword)
})
// 只监听params变化(比如动态路由参数)
watch(() => route.params, (newParams, oldParams) => {
  console.log('动态参数变了:', newParams, oldParams)
  // 比如用户页/user/:id,id变了请求新用户信息
  fetchUser(newParams.id)
})
</script>

好处:减少不必要的监听(比如路由path没变,只是query变了,监听整个$route会触发,但如果只关心query,这样更高效)。

处理“同路由组件复用”的坑

Vue Router有个特性:如果路由path相同,只是参数变化(比如/user/1/user/2),组件会被复用,此时组件的createdmounted等生命周期不会重新执行!这时候如果依赖路由参数初始化数据,就会出问题。

解决方法:用watch监听路由(上面讲的方法),或者在beforeRouteUpdate守卫里处理(Vue2的选项式API支持,Vue3的组合式API可以用onBeforeRouteUpdate)。

Vue3 组合式API示例

<script setup>
import { onBeforeRouteUpdate } from 'vue-router'
onBeforeRouteUpdate((to, from) => {
  // 路由更新前触发(此时组件还没销毁,适合更新数据)
  console.log('路由更新前:', to.params.id, from.params.id)
  fetchUser(to.params.id)
})
</script>

Vue2 选项式API示例

<script>
export default {
  beforeRouteUpdate(to, from, next) {
    // 处理参数变化逻辑
    this.fetchUser(to.params.id)
    next() // 必须调用next()放行
  }
}
</script>

路由变化时的细节:加载状态、错误、过渡

除了“监听变化执行逻辑”,还要考虑用户体验相关的细节,比如路由切换时的加载动画、错误处理。

路由懒加载的加载状态

import()懒加载组件时,路由跳转可能有延迟,需要给用户反馈(比如加载中动画),可以结合全局守卫和状态管理:

// router/index.js
const router = createRouter({...})
// 全局变量或Pinia/Vuex状态,记录是否在加载中
let isLoading = false
router.beforeEach((to, from, next) => {
  isLoading = true // 跳转前设为true,显示加载动画
  next()
})
router.afterEach(() => {
  setTimeout(() => { // 模拟延迟,实际看异步组件加载时间
    isLoading = false // 跳转后设为false,隐藏加载动画
  }, 300)
})

然后在App.vue里根据isLoading显示加载动画:

<template>
  <div v-if="isLoading" class="loading">加载中...</div>
  <router-view />
</template>
<script setup>
import { ref } from 'vue'
import { useRouter } from 'vue-router'
const isLoading = ref(false)
const router = useRouter()
router.beforeEach(() => { isLoading.value = true })
router.afterEach(() => { 
  setTimeout(() => { isLoading.value = false }, 300)
})
</script>

路由错误的捕获:onError

前面提过router.onError,可以捕获异步组件加载失败、路由配置错误等问题。

router.onError((error) => {
  console.error('路由错误:', error)
  // 跳转到自定义错误页
  router.push({ name: 'Error', params: { code: 500 }, query: { msg: '路由加载失败' } })
})

适用场景:生产环境中,避免白屏,给用户友好的错误提示。

路由切换的过渡动画

结合Vue的<transition><transition-group>,在路由变化时触发动画,思路是:给<router-view>包一层过渡组件,根据路由变化方向(比如从首页到列表页是左滑,返回是右滑)动态切换动画。

示例(简单淡入淡出)

<template>
  <transition name="fade">
    <router-view />
  </transition>
</template>
<style>
.fade-enter-from, .fade-leave-to {
  opacity: 0;
}
.fade-enter-active, .fade-leave-active {
  transition: opacity 0.3s;
}
</style>

更复杂的动画可以结合路由的meta字段,标记页面进入/离开的动画类型,在<transition>name里动态绑定。

实战场景:路由变化的组合玩法

光讲基础不够,结合实际项目需求,看看路由变化怎么和其他技术结合。

结合Pinia/Vuex管理全局状态

比如多标签页应用,路由变化时同步标签栏的选中状态:

// store/tabs.js(Pinia示例)
import { defineStore } from 'pinia'
export const useTabsStore = defineStore('tabs', {
  state: () => ({
    activeTab: ''
  }),
  actions: {
    setActiveTab(path) {
      this.activeTab = path
    }
  }
})

然后在全局守卫里更新状态:

// router/index.js
import { useTabsStore } from '@/store/tabs'
router.afterEach((to) => {
  const tabsStore = useTabsStore()
  tabsStore.setActiveTab(to.path)
})

这样标签栏就能根据路由变化,自动高亮当前激活的标签。

路由变化时的表单守卫(离开页面前提示)

用户编辑表单时,路由变化(比如点了其他菜单)要提示“是否放弃修改”,可以用beforeRouteLeave守卫(Vue2选项式或Vue3组合式):

Vue3 组合式API示例

<script setup>
import { onBeforeRouteLeave } from 'vue-router'
import { ref } from 'vue'
const formDirty = ref(false) // 表单是否有未保存修改
onBeforeRouteLeave((to, from, next) => {
  if (formDirty.value) {
    const confirm = window.confirm('有未保存内容,确定离开?')
    if (confirm) {
      next() // 确定离开
    } else {
      next(false) // 取消离开,留在当前页
    }
  } else {
    next() // 没有未保存内容,直接离开
  }
})
</script>

Vue2选项式API写法类似,用beforeRouteLeave钩子。

动态路由的权限细化

比如后台管理系统,不同角色能访问的路由不同,除了全局beforeEach,还可以在路由配置的meta里加更细的权限:

// 路由配置
{
  path: '/admin',
  name: 'Admin',
  component: () => import('./views/Admin.vue'),
  meta: {
    requiresAuth: true,
    role: 'admin' // 只有admin角色能进
  }
}

然后在beforeEach里判断:

router.beforeEach((to, from, next) => {
  const isLogin = localStorage.getItem('token')
  if (to.meta.requiresAuth && !isLogin) {
    next({ name: 'Login' })
  } else if (to.meta.role) {
    const userRole = localStorage.getItem('role')
    if (userRole !== to.meta.role) {
      next({ name: 'Forbidden' }) // 权限不足跳403页
    } else {
      next()
    }
  } else {
    next()
  }
})

常见问题&避坑指南

讲了这么多方法,实际开发中容易踩哪些坑?怎么解决?

监听$route不生效?

  • 原因1:没在Vue组件实例内监听,比如在普通JS文件里用watch(route, ...),但route是通过useRoute()获取的,必须在组件的<script setup>或选项式API的watch里用。
    解决:确保watch逻辑写在组件内部。

  • 原因2:路由是同一路由(path相同),组件复用导致watch没触发,比如/user/1/user/2,组件没销毁,此时要监听route.params或用beforeRouteUpdate
    解决:改用监听route.params,或者用onBeforeRouteUpdate

导航守卫执行顺序搞反了?

全局守卫、路由独享守卫、组件内守卫的执行顺序是:
beforeEach(全局)→ beforeEnter(路由独享)→ beforeRouteEnter(组件内)→ beforeResolve(全局)→ 组件渲染 → afterEach(全局)

如果逻辑依赖顺序,比如权限判断在全局,组件内要处理参数,得注意顺序。

避坑:画个执行顺序图,或者写注释明确每个守卫的作用。

路由懒加载时,beforeEach里拿不到组件实例?

因为懒加载的组件是异步加载的,beforeEach执行时组件可能还没加载,所以to.matched里的组件可能为空。

解决:如果需要依赖组件信息,用beforeResolve(在组件解析后执行)。

用了keep-alive,路由变化时组件不更新?

<keep-alive>会缓存组件实例,路由变化时如果组件被缓存,createdmounted不会重新执行。

解决:结合activated生命周期(组件被激活时执行),或者在watch里监听路由。

选对方法,高效监听路由变化

不同场景选不同方案:

  • 全局逻辑(权限、埋点、错误处理)→ 用导航守卫beforeEachafterEachonError等);
  • 组件内逻辑(参数变化、表单守卫)→ 用watch($route)组件内守卫beforeRouteUpdatebeforeRouteLeave);
  • 细节体验(加载动画、过渡)→ 结合导航守卫+状态管理+Vue过渡组件;

记住核心逻辑:路由变化是“跳转前→跳转中→跳转后”的过程,每个阶段都有对应的钩子,按需选择,同路由组件复用是常见坑,要针对性处理(监听params/query、用beforeRouteUpdate)。

多结合实际项目需求,比如权限、埋点、表单提示这些场景,把路由监听和业务逻辑无缝衔接,才能写出流畅的交互~

版权声明

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

发表评论:

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

热门