一、先想清楚,为什么要监听路由变化?
做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前端网发表,如需转载,请注明页面地址。
code前端网



