一、先想清楚,为什么要监听路由变化?
做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变了要重新请求商品数据。
监听路由的特定属性(比如query
、params
)
如果只关心query
或params
的变化,不用监听整个$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
),组件会被复用,此时组件的created
、mounted
等生命周期不会重新执行!这时候如果依赖路由参数初始化数据,就会出问题。
解决方法:用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>
会缓存组件实例,路由变化时如果组件被缓存,created
、mounted
不会重新执行。
解决:结合activated
生命周期(组件被激活时执行),或者在watch
里监听路由。
选对方法,高效监听路由变化
不同场景选不同方案:
- 全局逻辑(权限、埋点、错误处理)→ 用导航守卫(
beforeEach
、afterEach
、onError
等); - 组件内逻辑(参数变化、表单守卫)→ 用
watch($route)
或 组件内守卫(beforeRouteUpdate
、beforeRouteLeave
); - 细节体验(加载动画、过渡)→ 结合导航守卫+状态管理+Vue过渡组件;
记住核心逻辑:路由变化是“跳转前→跳转中→跳转后”的过程,每个阶段都有对应的钩子,按需选择,同路由组件复用是常见坑,要针对性处理(监听params
/query
、用beforeRouteUpdate
)。
多结合实际项目需求,比如权限、埋点、表单提示这些场景,把路由监听和业务逻辑无缝衔接,才能写出流畅的交互~
版权声明
本文仅代表作者观点,不代表Code前端网立场。
本文系作者Code前端网发表,如需转载,请注明页面地址。
发表评论:
◎欢迎参与讨论,请在这里发表您的看法、交流您的观点。